From 47cea6eae98d01ee817e213bd8de896c56a6f4d5 Mon Sep 17 00:00:00 2001 From: MengYX Date: Tue, 11 Feb 2020 00:34:26 +0800 Subject: [PATCH] Use Universal Mask for Qmc,Mgg,Mflac Add Local Experimental Support For Mgg --- src/decrypt/mflac.js | 102 +++------------------ src/decrypt/mgg.js | 52 +++-------- src/decrypt/qmc.js | 53 ++--------- src/decrypt/qmcmask.js | 200 +++++++++++++++++++++++++++++++++++++++++ src/decrypt/util.js | 35 ++++---- 5 files changed, 252 insertions(+), 190 deletions(-) create mode 100644 src/decrypt/qmcmask.js diff --git a/src/decrypt/mflac.js b/src/decrypt/mflac.js index 887808e..d012f03 100644 --- a/src/decrypt/mflac.js +++ b/src/decrypt/mflac.js @@ -1,36 +1,34 @@ const musicMetadata = require("music-metadata-browser"); -const util = require("./util"); -export {Decrypt} -const FLAC_HEADER = [0x66, 0x4C, 0x61, 0x43, 0x00]; +import {GetArrayBuffer, GetCoverURL, GetFileInfo} from "./util" -async function Decrypt(file, raw_filename, raw_ext) { +import * as mask from "./qmcmask" + +export async function Decrypt(file, raw_filename, raw_ext) { // 获取扩展名 if (raw_ext !== "mflac") return { status: false, message: "File type is incorrect!", }; // 读取文件 - const fileBuffer = await util.GetArrayBuffer(file); + const fileBuffer = await GetArrayBuffer(file); const audioData = new Uint8Array(fileBuffer.slice(0, -0x170)); - const audioDataLen = audioData.length; + //const audioDataLen = audioData.length; // 转换数据 - const seed = new Mask(); - if (!seed.DetectMask(audioData)) return { + const seed = mask.QmcMaskDetectMflac(audioData); + if (seed === undefined) return { status: false, message: "此音乐无法解锁,目前mflac格式不提供完整支持", }; - for (let cur = 0; cur < audioDataLen; ++cur) { - audioData[cur] ^= seed.NextMask(); - } + const dec = seed.Decrypt(audioData); // 导出 - const musicData = new Blob([audioData], {type: "audio/flac"}); + const musicData = new Blob([dec], {type: "audio/flac"}); // 读取Meta let tag = await musicMetadata.parseBlob(musicData); - const info = util.GetFileInfo(tag.common.artist, tag.common.title, raw_filename); - reportKeyInfo(new Uint8Array(fileBuffer.slice(-0x170)), seed.mask128, - info.artist, info.title, tag.common.album, raw_filename); + const info = GetFileInfo(tag.common.artist, tag.common.title, raw_filename); + //reportKeyInfo(new Uint8Array(fileBuffer.slice(-0x170)), seed.mask128, + // info.artist, info.title, tag.common.album, raw_filename); // 返回 return { @@ -39,84 +37,12 @@ async function Decrypt(file, raw_filename, raw_ext) { artist: info.artist, ext: 'flac', album: tag.common.album, - picture: util.GetCoverURL(tag), + picture: GetCoverURL(tag), file: URL.createObjectURL(musicData), mime: "audio/flac" } } -class Mask { - - - constructor() { - this.index = -1; - this.mask_index = -1; - this.mask128 = new Uint8Array(128); - this.mask58_martix = new Uint8Array(56); - this.mask58_super1 = 0x00; - this.mask58_super2 = 0x00; - } - - DetectMask(data) { - let search_len = Math.min(0x8000, data.length), mask; - for (let block_idx = 0; block_idx < search_len; block_idx += 128) { - mask = data.slice(block_idx, block_idx + 128); - const mask58 = this.Convert128to58(mask); - if (mask58 === undefined) continue; - - if (!FLAC_HEADER.every((val, idx) => { - return val === mask[idx] ^ data[idx]; - })) continue; - - this.mask128 = mask; - this.mask58_martix = mask58.matrix; - this.mask58_super1 = mask58.super_8_1; - this.mask58_super2 = mask58.super_8_2; - return true; - } - return false; - } - - Convert128to58(mask128) { - const super_8_1 = mask128[0], super_8_2 = mask128[8]; - let matrix = []; - for (let row_idx = 0; row_idx < 8; row_idx++) { - const len_start = 16 * row_idx; - const len_right_start = 120 - len_start;//16*(8-row_idx-1)+8 - - if (mask128[len_start] !== super_8_1 || mask128[len_start + 8] !== super_8_2) { - return - } - - const row_left = mask128.slice(len_start + 1, len_start + 8); - const row_right = mask128.slice(len_right_start + 1, len_right_start + 8).reverse(); - if (row_left.every((val, idx) => { - return row_right[idx] === val - })) { - matrix.push(row_left); - } else { - return - } - } - return {matrix, super_8_1, super_8_2} - } - - NextMask() { - this.index++; - this.mask_index++; - if (this.index === 0x8000 || (this.index > 0x8000 && (this.index + 1) % 0x8000 === 0)) { - this.index++; - this.mask_index++; - } - if (this.mask_index >= 128) { - this.mask_index -= 128; - } - return this.mask128[this.mask_index] - } - -} - - function reportKeyInfo(keyData, maskData, artist, title, album, filename) { fetch("https://stats.ixarea.com/collect/mflac/mask", { method: "POST", diff --git a/src/decrypt/mgg.js b/src/decrypt/mgg.js index 5a8ad1e..7de353a 100644 --- a/src/decrypt/mgg.js +++ b/src/decrypt/mgg.js @@ -1,9 +1,10 @@ const musicMetadata = require("music-metadata-browser"); const util = require("./util"); -export {Decrypt} -const FLAC_HEADER = [0x4F, 0x67, 0x67, 0x53, 0x00]; -async function Decrypt(file, raw_filename, raw_ext) { +import * as mask from "./qmcmask" + +//todo: combine qmc mflac mgg +export async function Decrypt(file, raw_filename, raw_ext) { // 获取扩展名 if (raw_ext !== "mgg") return { status: false, @@ -15,24 +16,21 @@ async function Decrypt(file, raw_filename, raw_ext) { const audioDataLen = audioData.length; const keyData = new Uint8Array(fileBuffer.slice(-0x170)); const headData = new Uint8Array(fileBuffer.slice(0, 170)); - let seed; - try { - let resp = await queryKeyInfo(keyData, headData, raw_filename); - let data = await resp.json(); - seed = new Mask(data.Mask); - - } catch (e) { + let seed = mask.QmcMaskDetectMgg(headData); + if (seed === undefined) { return { status: false, message: "此音乐无法解锁,目前mgg格式仅提供试验性支持", }; + /*try { + let resp = await queryKeyInfo(keyData, headData, raw_filename); + let data = await resp.json(); + seed = mask.QmcMaskCreate128(data.Mask); + } catch (e) {}*/ } - - for (let cur = 0; cur < audioDataLen; ++cur) { - audioData[cur] ^= seed.NextMask(); - } + const dec = seed.Decrypt(audioData); // 导出 - const musicData = new Blob([audioData], {type: "audio/ogg"}); + const musicData = new Blob([dec], {type: "audio/ogg"}); // 读取Meta let tag = await musicMetadata.parseBlob(musicData); @@ -52,31 +50,9 @@ async function Decrypt(file, raw_filename, raw_ext) { } -class Mask { - constructor(mask) { - this.index = -1; - this.mask_index = -1; - this.mask128 = mask; - } - - NextMask() { - this.index++; - this.mask_index++; - if (this.index === 0x8000 || (this.index > 0x8000 && (this.index + 1) % 0x8000 === 0)) { - this.index++; - this.mask_index++; - } - if (this.mask_index >= 128) { - this.mask_index -= 128; - } - return this.mask128[this.mask_index] - } - -} - function queryKeyInfo(keyData, headData, filename) { - return fetch("https://stats.ixarea.com/collect/mgg/query", { + return fetch("http://127.0.0.1:6580/mgg/query", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({Key: Array.from(keyData), Head: Array.from(headData), Filename: filename}), diff --git a/src/decrypt/qmc.js b/src/decrypt/qmc.js index 0a19b86..673d59e 100644 --- a/src/decrypt/qmc.js +++ b/src/decrypt/qmc.js @@ -1,15 +1,6 @@ const musicMetadata = require("music-metadata-browser"); const util = require("./util"); -export {Decrypt} -const SEED_MAP = [ - [0x4a, 0xd6, 0xca, 0x90, 0x67, 0xf7, 0x52], - [0x5e, 0x95, 0x23, 0x9f, 0x13, 0x11, 0x7e], - [0x47, 0x74, 0x3d, 0x90, 0xaa, 0x3f, 0x51], - [0xc6, 0x09, 0xd5, 0x9f, 0xfa, 0x66, 0xf9], - [0xf3, 0xd6, 0xa1, 0x90, 0xa0, 0xf7, 0xf0], - [0x1d, 0x95, 0xde, 0x9f, 0x84, 0x11, 0xf4], - [0x0e, 0x74, 0xbb, 0x90, 0xbc, 0x3f, 0x92], - [0x00, 0x09, 0x5b, 0x9f, 0x62, 0x66, 0xa1]]; +import * as mask from "./qmcmask" const OriginalExtMap = { "qmc0": "mp3", @@ -21,7 +12,8 @@ const OriginalExtMap = { "tkm": "m4a" }; -async function Decrypt(file, raw_filename, raw_ext) { +//todo: use header to detect media type +export async function Decrypt(file, raw_filename, raw_ext) { // 获取扩展名 if (!(raw_ext in OriginalExtMap)) { return {status: false, message: "File type is incorrect!"} @@ -32,12 +24,10 @@ async function Decrypt(file, raw_filename, raw_ext) { const fileBuffer = await util.GetArrayBuffer(file); const audioData = new Uint8Array(fileBuffer); // 转换数据 - const seed = new Mask(); - for (let cur = 0; cur < audioData.length; ++cur) { - audioData[cur] ^= seed.NextMask(); - } + const seed = mask.QmcMaskGetDefault(); + const dec = seed.Decrypt(audioData); // 导出 - const musicData = new Blob([audioData], {type: mime}); + const musicData = new Blob([dec], {type: mime}); // 读取Meta const tag = await musicMetadata.parseBlob(musicData); const info = util.GetFileInfo(tag.common.artist, tag.common.title, raw_filename); @@ -54,34 +44,3 @@ async function Decrypt(file, raw_filename, raw_ext) { mime: mime } } - -class Mask { - constructor() { - this.x = -1; - this.y = 8; - this.dx = 1; - this.index = -1; - } - - NextMask() { - let ret; - this.index++; - if (this.x < 0) { - this.dx = 1; - this.y = (8 - this.y) % 8; - ret = 0xc3 - } else if (this.x > 6) { - this.dx = -1; - this.y = 7 - this.y; - ret = 0xd8 - } else { - ret = SEED_MAP[this.y][this.x] - } - this.x += this.dx; - if (this.index === 0x8000 || (this.index > 0x8000 && (this.index + 1) % 0x8000 === 0)) { - return this.NextMask() - } - return ret - } - -} diff --git a/src/decrypt/qmcmask.js b/src/decrypt/qmcmask.js new file mode 100644 index 0000000..2a88f4f --- /dev/null +++ b/src/decrypt/qmcmask.js @@ -0,0 +1,200 @@ +import {FLAC_HEADER, IsBytesEqual} from "./util" + +const QMOggConstHeader = [ + 0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x1E, 0x01, 0x76, 0x6F, 0x72, + 0x62, 0x69, 0x73, 0x00, 0x00, 0x00, 0x00, 0x02, 0x44, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xEE, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0x01, 0x4F, 0x67, 0x67, 0x53, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x03, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73, 0x2C, 0x00, 0x00, 0x00, + 0x58, 0x69, 0x70, 0x68, 0x2E, 0x4F, 0x72, 0x67, 0x20, 0x6C, 0x69, 0x62, 0x56, 0x6F, 0x72, 0x62, + 0x69, 0x73, 0x20, 0x49, 0x20, 0x32, 0x30, 0x31, 0x35, 0x30, 0x31, 0x30, 0x35, 0x20, 0x28, 0xE2, + 0x9B, 0x84, 0xE2, 0x9B, 0x84, 0xE2, 0x9B, 0x84, 0xE2, 0x9B, 0x84, 0x29, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x54, 0x49, 0x54, 0x4C, 0x45, 0x3D]; +const QMOggConstHeaderConfidence = [ + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, + 0, 0, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 6, 3, 3, 3, 3, 6, 6, 6, 6, + 3, 3, 3, 3, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9, + 0, 0, 0, 0, 6, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 1, 9, 9, + 0, 1, 9, 9, 9, 9, 9, 9, 9, 9]; +const QMCDefaultMaskMatrix = [ + 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, 0x5E, + 0x95, 0x23, 0x9F, 0x13, 0x11, 0x7E, 0x47, 0x74, + 0x3D, 0x90, 0xAA, 0x3F, 0x51, 0xC6, 0x09, 0xD5, + 0x9F, 0xFA, 0x66, 0xF9, 0xF3, 0xD6, 0xA1, 0x90, + 0xA0, 0xF7, 0xF0, 0x1D, 0x95, 0xDE, 0x9F, 0x84, + 0x11, 0xF4, 0x0E, 0x74, 0xBB, 0x90, 0xBC, 0x3F, + 0x92, 0x00, 0x09, 0x5B, 0x9F, 0x62, 0x66, 0xA1]; +const QMCDefaultMaskSuperA = 0xC3; +const QMCDefaultMaskSuperB = 0xD8; + +class QmcMask { + constructor(matrix, superA, superB) { + if (superA === undefined || superB === undefined) { + this.Matrix128 = matrix; + this.generateMask58from128() + } else { + this.Matrix58 = matrix; + this.Super58A = superA; + this.Super58B = superB; + this.generateMask128from58(); + } + } + + generateMask128from58() { + if (this.Matrix58.length !== 56) throw "incorrect mask58 matrix length"; + + let matrix128 = []; + for (let rowIdx = 0; rowIdx < 8; rowIdx += 1) { + matrix128 = matrix128.concat( + [this.Super58A], + this.Matrix58.slice(7 * rowIdx, 7 * rowIdx + 7), + [this.Super58B], + this.Matrix58.slice(56 - 7 - 7 * rowIdx, 56 - 7 * rowIdx).reverse() + ); + } + this.Matrix128 = matrix128; + } + + generateMask58from128() { + if (this.Matrix128.length !== 128) throw "incorrect mask128 length"; + + const superA = this.Matrix128[0], superB = this.Matrix128[8]; + let matrix58 = []; + + for (let rowIdx = 0; rowIdx < 8; rowIdx += 1) { + let lenStart = 16 * rowIdx; + let lenRightStart = 120 - lenStart; + if (this.Matrix128[lenStart] !== superA || this.Matrix128[lenStart + 8] !== superB) { + throw "decode mask-128 to mask-58 failed" + } + let rowLeft = this.Matrix128.slice(lenStart + 1, lenStart + 8); + let rowRight = this.Matrix128.slice(lenRightStart + 1, lenRightStart + 8).reverse(); + if (IsBytesEqual(rowLeft, rowRight)) { + matrix58 = matrix58.concat(rowLeft); + } else { + throw "decode mask-128 to mask-58 failed" + } + } + this.Matrix58 = matrix58; + this.Super58A = superA; + this.Super58B = superB; + } + + Decrypt(data) { + let dst = data.slice(0); + let maskIdx = -1; + for (let cur = 0; cur < data.length; cur++) { + maskIdx++; + if (cur === 0x8001 || (cur > 0x8001 && cur % 0x8000 === 0)) maskIdx++; + if (maskIdx >= 128) maskIdx -= 128; + dst[cur] ^= this.Matrix128[maskIdx]; + } + return dst; + } +} + +export function QmcMaskGetDefault() { + return new QmcMask(QMCDefaultMaskMatrix, QMCDefaultMaskSuperA, QMCDefaultMaskSuperB) +} + +export function QmcMaskDetectMflac(data) { + let search_len = Math.min(0x8000, data.length), mask; + for (let block_idx = 0; block_idx < search_len; block_idx += 128) { + try { + mask = new QmcMask(data.slice(block_idx, block_idx + 128)); + if (!mask.Decrypt(data.slice(0, FLAC_HEADER.length)) + .every((val, idx) => { + return val === FLAC_HEADER[idx]; + })) break; + } catch (e) { + } + } + return mask +} + +export function QmcMaskDetectMgg(input) { + if (input.length < QMOggConstHeader.length) return; + let matrixConfidence = {}; + for (let i = 0; i < 58; i++) matrixConfidence[i] = {}; + + for (let idx128 = 0; idx128 < QMOggConstHeader.length; idx128++) { + if (QMOggConstHeaderConfidence[idx128] === 0) continue; + let idx58 = GetMask58Index(idx128); + let mask = input[idx128] ^ QMOggConstHeader[idx128]; + let confidence = QMOggConstHeaderConfidence[idx128]; + if (mask in matrixConfidence[idx58]) { + matrixConfidence[idx58][mask] += confidence + } else { + matrixConfidence[idx58][mask] = confidence + } + } + let matrix = [], superA, superB; + try { + for (let i = 0; i < 56; i++) matrix[i] = getMaskConfidenceResult(matrixConfidence[i]); + superA = getMaskConfidenceResult(matrixConfidence[56]); + superB = getMaskConfidenceResult(matrixConfidence[57]); + } catch (e) { + return; + } + return new QmcMask(matrix, superA, superB); +} + +export function QmcMaskCreate128(mask128) { + return new QmcMask(mask128) +} + +export function QmcMaskCreate58(matrix, superA, superB) { + return new QmcMask(matrix, superA, superB) +} + +/** + * @param confidence {{}} + * @returns {number} + */ +function getMaskConfidenceResult(confidence) { + if (confidence.length === 0) throw "can not match at least one key"; + let result = 0, conf = 0; + for (let idx in confidence) { + if (confidence[idx] > conf) { + result = idx; + conf = confidence[idx]; + } + } + return result +} + +/** + * @return {number} + */ +function GetMask58Index(idx128) { + if (idx128 > 127) idx128 = idx128 % 128; + let col = idx128 % 16; + let row = (idx128 - col) / 16; + switch (col) { + case 0://Super 1 + row = 8; + col = 0; + break; + case 8://Super 2 + row = 8; + col = 1; + break; + default: + if (col > 7) { + row = 7 - row; + col = 15 - col; + } else { + col -= 1; + } + break; + } + return row * 7 + col +} diff --git a/src/decrypt/util.js b/src/decrypt/util.js index 9f00b58..efbba68 100644 --- a/src/decrypt/util.js +++ b/src/decrypt/util.js @@ -1,7 +1,13 @@ -export {GetArrayBuffer, GetFileInfo, GetCoverURL, AudioMimeType} +export const AudioMimeType = { + mp3: "audio/mpeg", + flac: "audio/flac", + m4a: "audio/mp4", + ogg: "audio/ogg" +}; +export const FLAC_HEADER = [0x66, 0x4C, 0x61, 0x43, 0x00]; // Also a new draft API: blob.arrayBuffer() -async function GetArrayBuffer(blobObject) { +export async function GetArrayBuffer(blobObject) { return await new Promise(resolve => { const reader = new FileReader(); reader.onload = (e) => { @@ -11,14 +17,7 @@ async function GetArrayBuffer(blobObject) { }); } -const AudioMimeType = { - mp3: "audio/mpeg", - flac: "audio/flac", - m4a: "audio/mp4", - ogg: "audio/ogg" -}; - -function GetFileInfo(artist, title, filenameNoExt) { +export function GetFileInfo(artist, title, filenameNoExt) { let newArtist = "", newTitle = ""; let filenameArray = filenameNoExt.split("-"); if (filenameArray.length > 1) { @@ -28,19 +27,15 @@ function GetFileInfo(artist, title, filenameNoExt) { newTitle = filenameArray[0].trim(); } - if (typeof artist == "string" && artist !== "") { - newArtist = artist; - } - if (typeof title == "string" && title !== "") { - newTitle = title; - } + if (typeof artist == "string" && artist !== "") newArtist = artist; + if (typeof title == "string" && title !== "") newTitle = title; return {artist: newArtist, title: newTitle}; } /** * @return {string} */ -function GetCoverURL(metadata) { +export function GetCoverURL(metadata) { let pic_url = ""; if (metadata.common.picture !== undefined && metadata.common.picture.length > 0) { let pic = new Blob([metadata.common.picture[0].data], {type: metadata.common.picture[0].format}); @@ -48,3 +43,9 @@ function GetCoverURL(metadata) { } return pic_url; } + +export function IsBytesEqual(first, second) { + return first.every((val, idx) => { + return val === second[idx]; + }) +}