all: format with prettier
This commit is contained in:
@@ -1,73 +1,71 @@
|
||||
import {Decrypt as XmDecrypt} from "@/decrypt/xm";
|
||||
import {Decrypt as QmcDecrypt} from "@/decrypt/qmc";
|
||||
import {Decrypt as QmcCacheDecrypt} from "@/decrypt/qmccache";
|
||||
import {Decrypt as KgmDecrypt} from "@/decrypt/kgm";
|
||||
import {Decrypt as KwmDecrypt} from "@/decrypt/kwm";
|
||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
|
||||
import {Decrypt as TmDecrypt} from "@/decrypt/tm";
|
||||
import {DecryptResult, FileInfo} from "@/decrypt/entity";
|
||||
import {SplitFilename} from "@/decrypt/utils";
|
||||
|
||||
import { Decrypt as XmDecrypt } from '@/decrypt/xm';
|
||||
import { Decrypt as QmcDecrypt } from '@/decrypt/qmc';
|
||||
import { Decrypt as QmcCacheDecrypt } from '@/decrypt/qmccache';
|
||||
import { Decrypt as KgmDecrypt } from '@/decrypt/kgm';
|
||||
import { Decrypt as KwmDecrypt } from '@/decrypt/kwm';
|
||||
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
|
||||
import { Decrypt as TmDecrypt } from '@/decrypt/tm';
|
||||
import { DecryptResult, FileInfo } from '@/decrypt/entity';
|
||||
import { SplitFilename } from '@/decrypt/utils';
|
||||
|
||||
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
|
||||
const raw = SplitFilename(file.name)
|
||||
let rt_data: DecryptResult;
|
||||
switch (raw.ext) {
|
||||
case "kwm":// Kuwo Mp3/Flac
|
||||
rt_data = await KwmDecrypt(file.raw, raw.name, raw.ext);
|
||||
break
|
||||
case "xm": // Xiami Wav/M4a/Mp3/Flac
|
||||
case "wav":// Xiami/Raw Wav
|
||||
case "mp3":// Xiami/Raw Mp3
|
||||
case "flac":// Xiami/Raw Flac
|
||||
case "m4a":// Xiami/Raw M4a
|
||||
rt_data = await XmDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
case "ogg":// Raw Ogg
|
||||
rt_data = await RawDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
case "tm0":// QQ Music IOS Mp3
|
||||
case "tm3":// QQ Music IOS Mp3
|
||||
rt_data = await RawDecrypt(file.raw, raw.name, "mp3");
|
||||
break;
|
||||
case "qmc3"://QQ Music Android Mp3
|
||||
case "qmc2"://QQ Music Android Ogg
|
||||
case "qmc0"://QQ Music Android Mp3
|
||||
case "qmcflac"://QQ Music Android Flac
|
||||
case "qmcogg"://QQ Music Android Ogg
|
||||
case "tkm"://QQ Music Accompaniment M4a
|
||||
case "bkcmp3"://Moo Music Mp3
|
||||
case "bkcflac"://Moo Music Flac
|
||||
case "mflac"://QQ Music New Flac
|
||||
case "mflac0"://QQ Music New Flac
|
||||
case "mgg": //QQ Music New Ogg
|
||||
case "mgg1": //QQ Music New Ogg
|
||||
case "666c6163"://QQ Music Weiyun Flac
|
||||
case "6d7033"://QQ Music Weiyun Mp3
|
||||
case "6f6767"://QQ Music Weiyun Ogg
|
||||
case "6d3461"://QQ Music Weiyun M4a
|
||||
case "776176"://QQ Music Weiyun Wav
|
||||
rt_data = await QmcDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
case "tm2":// QQ Music IOS M4a
|
||||
case "tm6":// QQ Music IOS M4a
|
||||
rt_data = await TmDecrypt(file.raw, raw.name);
|
||||
break;
|
||||
case "cache"://QQ Music Cache
|
||||
rt_data = await QmcCacheDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
case "vpr":
|
||||
case "kgm":
|
||||
case "kgma":
|
||||
rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
|
||||
break
|
||||
default:
|
||||
throw "不支持此文件格式"
|
||||
}
|
||||
const raw = SplitFilename(file.name);
|
||||
let rt_data: DecryptResult;
|
||||
switch (raw.ext) {
|
||||
case 'kwm': // Kuwo Mp3/Flac
|
||||
rt_data = await KwmDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
case 'xm': // Xiami Wav/M4a/Mp3/Flac
|
||||
case 'wav': // Xiami/Raw Wav
|
||||
case 'mp3': // Xiami/Raw Mp3
|
||||
case 'flac': // Xiami/Raw Flac
|
||||
case 'm4a': // Xiami/Raw M4a
|
||||
rt_data = await XmDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
case 'ogg': // Raw Ogg
|
||||
rt_data = await RawDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
case 'tm0': // QQ Music IOS Mp3
|
||||
case 'tm3': // QQ Music IOS Mp3
|
||||
rt_data = await RawDecrypt(file.raw, raw.name, 'mp3');
|
||||
break;
|
||||
case 'qmc3': //QQ Music Android Mp3
|
||||
case 'qmc2': //QQ Music Android Ogg
|
||||
case 'qmc0': //QQ Music Android Mp3
|
||||
case 'qmcflac': //QQ Music Android Flac
|
||||
case 'qmcogg': //QQ Music Android Ogg
|
||||
case 'tkm': //QQ Music Accompaniment M4a
|
||||
case 'bkcmp3': //Moo Music Mp3
|
||||
case 'bkcflac': //Moo Music Flac
|
||||
case 'mflac': //QQ Music New Flac
|
||||
case 'mflac0': //QQ Music New Flac
|
||||
case 'mgg': //QQ Music New Ogg
|
||||
case 'mgg1': //QQ Music New Ogg
|
||||
case '666c6163': //QQ Music Weiyun Flac
|
||||
case '6d7033': //QQ Music Weiyun Mp3
|
||||
case '6f6767': //QQ Music Weiyun Ogg
|
||||
case '6d3461': //QQ Music Weiyun M4a
|
||||
case '776176': //QQ Music Weiyun Wav
|
||||
rt_data = await QmcDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
case 'tm2': // QQ Music IOS M4a
|
||||
case 'tm6': // QQ Music IOS M4a
|
||||
rt_data = await TmDecrypt(file.raw, raw.name);
|
||||
break;
|
||||
case 'cache': //QQ Music Cache
|
||||
rt_data = await QmcCacheDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
case 'vpr':
|
||||
case 'kgm':
|
||||
case 'kgma':
|
||||
rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext);
|
||||
break;
|
||||
default:
|
||||
throw '不支持此文件格式';
|
||||
}
|
||||
|
||||
if (!rt_data.rawExt) rt_data.rawExt = raw.ext;
|
||||
if (!rt_data.rawFilename) rt_data.rawFilename = raw.name;
|
||||
console.log(rt_data);
|
||||
return rt_data;
|
||||
if (!rt_data.rawExt) rt_data.rawExt = raw.ext;
|
||||
if (!rt_data.rawFilename) rt_data.rawFilename = raw.name;
|
||||
console.log(rt_data);
|
||||
return rt_data;
|
||||
}
|
||||
|
||||
|
@@ -1,26 +1,25 @@
|
||||
export interface DecryptResult {
|
||||
title: string
|
||||
album?: string
|
||||
artist?: string
|
||||
title: string;
|
||||
album?: string;
|
||||
artist?: string;
|
||||
|
||||
mime: string
|
||||
ext: string
|
||||
mime: string;
|
||||
ext: string;
|
||||
|
||||
file: string
|
||||
blob: Blob
|
||||
picture?: string
|
||||
|
||||
message?: string
|
||||
rawExt?: string
|
||||
rawFilename?: string
|
||||
file: string;
|
||||
blob: Blob;
|
||||
picture?: string;
|
||||
|
||||
message?: string;
|
||||
rawExt?: string;
|
||||
rawFilename?: string;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
status: string
|
||||
name: string,
|
||||
size: number,
|
||||
percentage: number,
|
||||
uid: number,
|
||||
raw: File
|
||||
status: string;
|
||||
name: string;
|
||||
size: number;
|
||||
percentage: number;
|
||||
uid: number;
|
||||
raw: File;
|
||||
}
|
||||
|
@@ -1,122 +1,125 @@
|
||||
import {
|
||||
AudioMimeType,
|
||||
BytesHasPrefix,
|
||||
GetArrayBuffer,
|
||||
GetCoverFromFile,
|
||||
GetMetaFromFile,
|
||||
SniffAudioExt
|
||||
} from "@/decrypt/utils";
|
||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
||||
import {DecryptResult} from "@/decrypt/entity";
|
||||
import config from "@/../package.json"
|
||||
AudioMimeType,
|
||||
BytesHasPrefix,
|
||||
GetArrayBuffer,
|
||||
GetCoverFromFile,
|
||||
GetMetaFromFile,
|
||||
SniffAudioExt,
|
||||
} from '@/decrypt/utils';
|
||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
import config from '@/../package.json';
|
||||
|
||||
//prettier-ignore
|
||||
const VprHeader = [
|
||||
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
|
||||
0x91, 0xAA, 0xBD, 0xD0, 0x7A, 0xF5, 0x36, 0x31]
|
||||
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
|
||||
0x91, 0xAA, 0xBD, 0xD0, 0x7A, 0xF5, 0x36, 0x31
|
||||
]
|
||||
//prettier-ignore
|
||||
const KgmHeader = [
|
||||
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
|
||||
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14]
|
||||
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
|
||||
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14
|
||||
]
|
||||
//prettier-ignore
|
||||
const VprMaskDiff = [
|
||||
0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
|
||||
0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
|
||||
0x00]
|
||||
|
||||
0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
|
||||
0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
|
||||
0x00
|
||||
]
|
||||
|
||||
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
||||
if (raw_ext === 'vpr') {
|
||||
if (!BytesHasPrefix(oriData, VprHeader)) throw Error('Not a valid vpr file!');
|
||||
} else {
|
||||
if (!BytesHasPrefix(oriData, KgmHeader)) throw Error('Not a valid kgm(a) file!');
|
||||
}
|
||||
let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer);
|
||||
let headerLen = bHeaderLen.getUint32(0, true);
|
||||
|
||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
||||
if (raw_ext === "vpr") {
|
||||
if (!BytesHasPrefix(oriData, VprHeader)) throw Error("Not a valid vpr file!")
|
||||
} else {
|
||||
if (!BytesHasPrefix(oriData, KgmHeader)) throw Error("Not a valid kgm(a) file!")
|
||||
}
|
||||
let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer)
|
||||
let headerLen = bHeaderLen.getUint32(0, true)
|
||||
let audioData = oriData.slice(headerLen);
|
||||
let dataLen = audioData.length;
|
||||
if (audioData.byteLength > 1 << 26) {
|
||||
throw Error("文件过大,请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁");
|
||||
}
|
||||
|
||||
let audioData = oriData.slice(headerLen)
|
||||
let dataLen = audioData.length
|
||||
if (audioData.byteLength > 1 << 26) {
|
||||
throw Error("文件过大,请使用 <a target='_blank' href='https://github.com/unlock-music/cli'>CLI版本</a> 进行解锁")
|
||||
}
|
||||
let key1 = new Uint8Array(17);
|
||||
key1.set(oriData.slice(0x1c, 0x2c), 0);
|
||||
if (MaskV2.length === 0) {
|
||||
if (!(await LoadMaskV2())) throw Error('加载Kgm/Vpr Mask数据失败');
|
||||
}
|
||||
|
||||
let key1 = new Uint8Array(17)
|
||||
key1.set(oriData.slice(0x1c, 0x2c), 0)
|
||||
if (MaskV2.length === 0) {
|
||||
if (!await LoadMaskV2()) throw Error("加载Kgm/Vpr Mask数据失败")
|
||||
}
|
||||
for (let i = 0; i < dataLen; i++) {
|
||||
let med8 = key1[i % 17] ^ audioData[i];
|
||||
med8 ^= (med8 & 0xf) << 4;
|
||||
|
||||
for (let i = 0; i < dataLen; i++) {
|
||||
let med8 = key1[i % 17] ^ audioData[i]
|
||||
med8 ^= (med8 & 0xf) << 4
|
||||
let msk8 = GetMask(i);
|
||||
msk8 ^= (msk8 & 0xf) << 4;
|
||||
audioData[i] = med8 ^ msk8;
|
||||
}
|
||||
if (raw_ext === 'vpr') {
|
||||
for (let i = 0; i < dataLen; i++) audioData[i] ^= VprMaskDiff[i % 17];
|
||||
}
|
||||
|
||||
let msk8 = GetMask(i)
|
||||
msk8 ^= (msk8 & 0xf) << 4
|
||||
audioData[i] = med8 ^ msk8
|
||||
}
|
||||
if (raw_ext === "vpr") {
|
||||
for (let i = 0; i < dataLen; i++) audioData[i] ^= VprMaskDiff[i % 17]
|
||||
}
|
||||
|
||||
const ext = SniffAudioExt(audioData);
|
||||
const mime = AudioMimeType[ext];
|
||||
let musicBlob = new Blob([audioData], {type: mime});
|
||||
const musicMeta = await metaParseBlob(musicBlob);
|
||||
const {title, artist} = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist)
|
||||
return {
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
ext,
|
||||
mime,
|
||||
title,
|
||||
artist
|
||||
}
|
||||
const ext = SniffAudioExt(audioData);
|
||||
const mime = AudioMimeType[ext];
|
||||
let musicBlob = new Blob([audioData], { type: mime });
|
||||
const musicMeta = await metaParseBlob(musicBlob);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
|
||||
return {
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
ext,
|
||||
mime,
|
||||
title,
|
||||
artist,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function GetMask(pos: number) {
|
||||
return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4]
|
||||
return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4];
|
||||
}
|
||||
|
||||
let MaskV2: Uint8Array = new Uint8Array(0);
|
||||
|
||||
async function LoadMaskV2(): Promise<boolean> {
|
||||
let mask_url = `https://cdn.jsdelivr.net/gh/unlock-music/unlock-music@${config.version}/public/static/kgm.mask`
|
||||
if (["http:", "https:"].some(v => v == self.location.protocol)) {
|
||||
if (!!self.document) {// using Web Worker
|
||||
mask_url = "./static/kgm.mask"
|
||||
} else {// using Main thread
|
||||
mask_url = "../static/kgm.mask"
|
||||
}
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(mask_url, {method: "GET"})
|
||||
MaskV2 = new Uint8Array(await resp.arrayBuffer());
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
let mask_url = `https://cdn.jsdelivr.net/gh/unlock-music/unlock-music@${config.version}/public/static/kgm.mask`;
|
||||
if (['http:', 'https:'].some((v) => v == self.location.protocol)) {
|
||||
if (!!self.document) {
|
||||
// using Web Worker
|
||||
mask_url = './static/kgm.mask';
|
||||
} else {
|
||||
// using Main thread
|
||||
mask_url = '../static/kgm.mask';
|
||||
}
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(mask_url, { method: 'GET' });
|
||||
MaskV2 = new Uint8Array(await resp.arrayBuffer());
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//prettier-ignore
|
||||
const MaskV2PreDef = [
|
||||
0xB8, 0xD5, 0x3D, 0xB2, 0xE9, 0xAF, 0x78, 0x8C, 0x83, 0x33, 0x71, 0x51, 0x76, 0xA0, 0xCD, 0x37,
|
||||
0x2F, 0x3E, 0x35, 0x8D, 0xA9, 0xBE, 0x98, 0xB7, 0xE7, 0x8C, 0x22, 0xCE, 0x5A, 0x61, 0xDF, 0x68,
|
||||
0x69, 0x89, 0xFE, 0xA5, 0xB6, 0xDE, 0xA9, 0x77, 0xFC, 0xC8, 0xBD, 0xBD, 0xE5, 0x6D, 0x3E, 0x5A,
|
||||
0x36, 0xEF, 0x69, 0x4E, 0xBE, 0xE1, 0xE9, 0x66, 0x1C, 0xF3, 0xD9, 0x02, 0xB6, 0xF2, 0x12, 0x9B,
|
||||
0x44, 0xD0, 0x6F, 0xB9, 0x35, 0x89, 0xB6, 0x46, 0x6D, 0x73, 0x82, 0x06, 0x69, 0xC1, 0xED, 0xD7,
|
||||
0x85, 0xC2, 0x30, 0xDF, 0xA2, 0x62, 0xBE, 0x79, 0x2D, 0x62, 0x62, 0x3D, 0x0D, 0x7E, 0xBE, 0x48,
|
||||
0x89, 0x23, 0x02, 0xA0, 0xE4, 0xD5, 0x75, 0x51, 0x32, 0x02, 0x53, 0xFD, 0x16, 0x3A, 0x21, 0x3B,
|
||||
0x16, 0x0F, 0xC3, 0xB2, 0xBB, 0xB3, 0xE2, 0xBA, 0x3A, 0x3D, 0x13, 0xEC, 0xF6, 0x01, 0x45, 0x84,
|
||||
0xA5, 0x70, 0x0F, 0x93, 0x49, 0x0C, 0x64, 0xCD, 0x31, 0xD5, 0xCC, 0x4C, 0x07, 0x01, 0x9E, 0x00,
|
||||
0x1A, 0x23, 0x90, 0xBF, 0x88, 0x1E, 0x3B, 0xAB, 0xA6, 0x3E, 0xC4, 0x73, 0x47, 0x10, 0x7E, 0x3B,
|
||||
0x5E, 0xBC, 0xE3, 0x00, 0x84, 0xFF, 0x09, 0xD4, 0xE0, 0x89, 0x0F, 0x5B, 0x58, 0x70, 0x4F, 0xFB,
|
||||
0x65, 0xD8, 0x5C, 0x53, 0x1B, 0xD3, 0xC8, 0xC6, 0xBF, 0xEF, 0x98, 0xB0, 0x50, 0x4F, 0x0F, 0xEA,
|
||||
0xE5, 0x83, 0x58, 0x8C, 0x28, 0x2C, 0x84, 0x67, 0xCD, 0xD0, 0x9E, 0x47, 0xDB, 0x27, 0x50, 0xCA,
|
||||
0xF4, 0x63, 0x63, 0xE8, 0x97, 0x7F, 0x1B, 0x4B, 0x0C, 0xC2, 0xC1, 0x21, 0x4C, 0xCC, 0x58, 0xF5,
|
||||
0x94, 0x52, 0xA3, 0xF3, 0xD3, 0xE0, 0x68, 0xF4, 0x00, 0x23, 0xF3, 0x5E, 0x0A, 0x7B, 0x93, 0xDD,
|
||||
0xAB, 0x12, 0xB2, 0x13, 0xE8, 0x84, 0xD7, 0xA7, 0x9F, 0x0F, 0x32, 0x4C, 0x55, 0x1D, 0x04, 0x36,
|
||||
0x52, 0xDC, 0x03, 0xF3, 0xF9, 0x4E, 0x42, 0xE9, 0x3D, 0x61, 0xEF, 0x7C, 0xB6, 0xB3, 0x93, 0x50,
|
||||
]
|
||||
|
||||
0xb8, 0xd5, 0x3d, 0xb2, 0xe9, 0xaf, 0x78, 0x8c, 0x83, 0x33, 0x71, 0x51, 0x76, 0xa0, 0xcd, 0x37, 0x2f, 0x3e, 0x35,
|
||||
0x8d, 0xa9, 0xbe, 0x98, 0xb7, 0xe7, 0x8c, 0x22, 0xce, 0x5a, 0x61, 0xdf, 0x68, 0x69, 0x89, 0xfe, 0xa5, 0xb6, 0xde,
|
||||
0xa9, 0x77, 0xfc, 0xc8, 0xbd, 0xbd, 0xe5, 0x6d, 0x3e, 0x5a, 0x36, 0xef, 0x69, 0x4e, 0xbe, 0xe1, 0xe9, 0x66, 0x1c,
|
||||
0xf3, 0xd9, 0x02, 0xb6, 0xf2, 0x12, 0x9b, 0x44, 0xd0, 0x6f, 0xb9, 0x35, 0x89, 0xb6, 0x46, 0x6d, 0x73, 0x82, 0x06,
|
||||
0x69, 0xc1, 0xed, 0xd7, 0x85, 0xc2, 0x30, 0xdf, 0xa2, 0x62, 0xbe, 0x79, 0x2d, 0x62, 0x62, 0x3d, 0x0d, 0x7e, 0xbe,
|
||||
0x48, 0x89, 0x23, 0x02, 0xa0, 0xe4, 0xd5, 0x75, 0x51, 0x32, 0x02, 0x53, 0xfd, 0x16, 0x3a, 0x21, 0x3b, 0x16, 0x0f,
|
||||
0xc3, 0xb2, 0xbb, 0xb3, 0xe2, 0xba, 0x3a, 0x3d, 0x13, 0xec, 0xf6, 0x01, 0x45, 0x84, 0xa5, 0x70, 0x0f, 0x93, 0x49,
|
||||
0x0c, 0x64, 0xcd, 0x31, 0xd5, 0xcc, 0x4c, 0x07, 0x01, 0x9e, 0x00, 0x1a, 0x23, 0x90, 0xbf, 0x88, 0x1e, 0x3b, 0xab,
|
||||
0xa6, 0x3e, 0xc4, 0x73, 0x47, 0x10, 0x7e, 0x3b, 0x5e, 0xbc, 0xe3, 0x00, 0x84, 0xff, 0x09, 0xd4, 0xe0, 0x89, 0x0f,
|
||||
0x5b, 0x58, 0x70, 0x4f, 0xfb, 0x65, 0xd8, 0x5c, 0x53, 0x1b, 0xd3, 0xc8, 0xc6, 0xbf, 0xef, 0x98, 0xb0, 0x50, 0x4f,
|
||||
0x0f, 0xea, 0xe5, 0x83, 0x58, 0x8c, 0x28, 0x2c, 0x84, 0x67, 0xcd, 0xd0, 0x9e, 0x47, 0xdb, 0x27, 0x50, 0xca, 0xf4,
|
||||
0x63, 0x63, 0xe8, 0x97, 0x7f, 0x1b, 0x4b, 0x0c, 0xc2, 0xc1, 0x21, 0x4c, 0xcc, 0x58, 0xf5, 0x94, 0x52, 0xa3, 0xf3,
|
||||
0xd3, 0xe0, 0x68, 0xf4, 0x00, 0x23, 0xf3, 0x5e, 0x0a, 0x7b, 0x93, 0xdd, 0xab, 0x12, 0xb2, 0x13, 0xe8, 0x84, 0xd7,
|
||||
0xa7, 0x9f, 0x0f, 0x32, 0x4c, 0x55, 0x1d, 0x04, 0x36, 0x52, 0xdc, 0x03, 0xf3, 0xf9, 0x4e, 0x42, 0xe9, 0x3d, 0x61,
|
||||
0xef, 0x7c, 0xb6, 0xb3, 0x93, 0x50,
|
||||
];
|
||||
|
@@ -1,77 +1,74 @@
|
||||
import {
|
||||
AudioMimeType,
|
||||
BytesHasPrefix,
|
||||
GetArrayBuffer,
|
||||
GetCoverFromFile,
|
||||
GetMetaFromFile,
|
||||
SniffAudioExt
|
||||
} from "@/decrypt/utils";
|
||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
|
||||
AudioMimeType,
|
||||
BytesHasPrefix,
|
||||
GetArrayBuffer,
|
||||
GetCoverFromFile,
|
||||
GetMetaFromFile,
|
||||
SniffAudioExt,
|
||||
} from '@/decrypt/utils';
|
||||
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
|
||||
|
||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
||||
import {DecryptResult} from "@/decrypt/entity";
|
||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
|
||||
//prettier-ignore
|
||||
const MagicHeader = [
|
||||
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
|
||||
0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65,
|
||||
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
|
||||
0x6B, 0x75, 0x77, 0x6F, 0x2D, 0x74, 0x6D, 0x65,
|
||||
]
|
||||
const PreDefinedKey = "MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk"
|
||||
const PreDefinedKey = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk';
|
||||
|
||||
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
|
||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
||||
if (!BytesHasPrefix(oriData, MagicHeader)) {
|
||||
if (SniffAudioExt(oriData) === "aac") {
|
||||
return await RawDecrypt(file, raw_filename, "aac", false)
|
||||
}
|
||||
throw Error("not a valid kwm file")
|
||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
||||
if (!BytesHasPrefix(oriData, MagicHeader)) {
|
||||
if (SniffAudioExt(oriData) === 'aac') {
|
||||
return await RawDecrypt(file, raw_filename, 'aac', false);
|
||||
}
|
||||
throw Error('not a valid kwm file');
|
||||
}
|
||||
|
||||
let fileKey = oriData.slice(0x18, 0x20)
|
||||
let mask = createMaskFromKey(fileKey)
|
||||
let audioData = oriData.slice(0x400);
|
||||
let lenAudioData = audioData.length;
|
||||
for (let cur = 0; cur < lenAudioData; ++cur)
|
||||
audioData[cur] ^= mask[cur % 0x20];
|
||||
let fileKey = oriData.slice(0x18, 0x20);
|
||||
let mask = createMaskFromKey(fileKey);
|
||||
let audioData = oriData.slice(0x400);
|
||||
let lenAudioData = audioData.length;
|
||||
for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= mask[cur % 0x20];
|
||||
|
||||
const ext = SniffAudioExt(audioData);
|
||||
const mime = AudioMimeType[ext];
|
||||
let musicBlob = new Blob([audioData], { type: mime });
|
||||
|
||||
const ext = SniffAudioExt(audioData);
|
||||
const mime = AudioMimeType[ext];
|
||||
let musicBlob = new Blob([audioData], {type: mime});
|
||||
|
||||
const musicMeta = await metaParseBlob(musicBlob);
|
||||
const {title, artist} = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist)
|
||||
return {
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
mime,
|
||||
title,
|
||||
artist,
|
||||
ext
|
||||
}
|
||||
const musicMeta = await metaParseBlob(musicBlob);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
|
||||
return {
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
mime,
|
||||
title,
|
||||
artist,
|
||||
ext,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function createMaskFromKey(keyBytes: Uint8Array): Uint8Array {
|
||||
let keyView = new DataView(keyBytes.buffer)
|
||||
let keyStr = keyView.getBigUint64(0, true).toString()
|
||||
let keyStrTrim = trimKey(keyStr)
|
||||
let key = new Uint8Array(32)
|
||||
for (let i = 0; i < 32; i++) {
|
||||
key[i] = PreDefinedKey.charCodeAt(i) ^ keyStrTrim.charCodeAt(i)
|
||||
}
|
||||
return key
|
||||
let keyView = new DataView(keyBytes.buffer);
|
||||
let keyStr = keyView.getBigUint64(0, true).toString();
|
||||
let keyStrTrim = trimKey(keyStr);
|
||||
let key = new Uint8Array(32);
|
||||
for (let i = 0; i < 32; i++) {
|
||||
key[i] = PreDefinedKey.charCodeAt(i) ^ keyStrTrim.charCodeAt(i);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
function trimKey(keyRaw: string): string {
|
||||
let lenRaw = keyRaw.length;
|
||||
let out = keyRaw;
|
||||
if (lenRaw > 32) {
|
||||
out = keyRaw.slice(0, 32)
|
||||
} else if (lenRaw < 32) {
|
||||
out = keyRaw.padEnd(32, keyRaw)
|
||||
}
|
||||
return out
|
||||
let lenRaw = keyRaw.length;
|
||||
let out = keyRaw;
|
||||
if (lenRaw > 32) {
|
||||
out = keyRaw.slice(0, 32);
|
||||
} else if (lenRaw < 32) {
|
||||
out = keyRaw.padEnd(32, keyRaw);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import fs from "fs";
|
||||
import {QmcDecoder} from "@/decrypt/qmc";
|
||||
import {BytesEqual} from "@/decrypt/utils";
|
||||
import fs from 'fs';
|
||||
import { QmcDecoder } from '@/decrypt/qmc';
|
||||
import { BytesEqual } from '@/decrypt/utils';
|
||||
|
||||
function loadTestDataDecoder(name: string): {
|
||||
cipherText: Uint8Array,
|
||||
clearText: Uint8Array
|
||||
cipherText: Uint8Array;
|
||||
clearText: Uint8Array;
|
||||
} {
|
||||
const cipherBody = fs.readFileSync(`./testdata/${name}_raw.bin`);
|
||||
const cipherSuffix = fs.readFileSync(`./testdata/${name}_suffix.bin`);
|
||||
@@ -13,20 +13,17 @@ function loadTestDataDecoder(name: string): {
|
||||
cipherText.set(cipherSuffix, cipherBody.length);
|
||||
return {
|
||||
cipherText,
|
||||
clearText: fs.readFileSync(`testdata/${name}_target.bin`)
|
||||
}
|
||||
clearText: fs.readFileSync(`testdata/${name}_target.bin`),
|
||||
};
|
||||
}
|
||||
|
||||
test("qmc: real file", async () => {
|
||||
const cases = ["mflac0_rc4", "mflac_map", "mgg_map", "qmc0_static"]
|
||||
test('qmc: real file', async () => {
|
||||
const cases = ['mflac0_rc4', 'mflac_map', 'mgg_map', 'qmc0_static'];
|
||||
for (const name of cases) {
|
||||
const {clearText, cipherText} = loadTestDataDecoder(name)
|
||||
const c = new QmcDecoder(cipherText)
|
||||
const buf = c.decrypt()
|
||||
const { clearText, cipherText } = loadTestDataDecoder(name);
|
||||
const c = new QmcDecoder(cipherText);
|
||||
const buf = c.decrypt();
|
||||
|
||||
expect(BytesEqual(buf, clearText)).toBeTruthy()
|
||||
expect(BytesEqual(buf, clearText)).toBeTruthy();
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher} from "./qmc_cipher";
|
||||
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher';
|
||||
import {
|
||||
AudioMimeType,
|
||||
GetArrayBuffer,
|
||||
@@ -7,44 +7,43 @@ import {
|
||||
GetMetaFromFile,
|
||||
SniffAudioExt,
|
||||
WriteMetaToFlac,
|
||||
WriteMetaToMp3
|
||||
} from "@/decrypt/utils";
|
||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
||||
import {DecryptQMCWasm} from "./qmc_wasm";
|
||||
WriteMetaToMp3,
|
||||
} from '@/decrypt/utils';
|
||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||
import { DecryptQMCWasm } from './qmc_wasm';
|
||||
|
||||
|
||||
import iconv from "iconv-lite";
|
||||
import {DecryptResult} from "@/decrypt/entity";
|
||||
import {queryAlbumCover} from "@/utils/api";
|
||||
import {QmcDeriveKey} from "@/decrypt/qmc_key";
|
||||
import iconv from 'iconv-lite';
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
import { queryAlbumCover } from '@/utils/api';
|
||||
import { QmcDeriveKey } from '@/decrypt/qmc_key';
|
||||
|
||||
interface Handler {
|
||||
ext: string
|
||||
version: number
|
||||
ext: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export const HandlerMap: { [key: string]: Handler } = {
|
||||
"mgg": {ext: "ogg", version: 2},
|
||||
"mgg1": {ext: "ogg", version: 2},
|
||||
"mflac": {ext: "flac", version: 2},
|
||||
"mflac0": {ext: "flac", version: 2},
|
||||
mgg: { ext: 'ogg', version: 2 },
|
||||
mgg1: { ext: 'ogg', version: 2 },
|
||||
mflac: { ext: 'flac', version: 2 },
|
||||
mflac0: { ext: 'flac', version: 2 },
|
||||
|
||||
// qmcflac / qmcogg:
|
||||
// 有可能是 v2 加密但混用同一个后缀名。
|
||||
"qmcflac": {ext: "flac", version: 2},
|
||||
"qmcogg": {ext: "ogg", version: 2},
|
||||
qmcflac: { ext: 'flac', version: 2 },
|
||||
qmcogg: { ext: 'ogg', version: 2 },
|
||||
|
||||
"qmc0": {ext: "mp3", version: 1},
|
||||
"qmc2": {ext: "ogg", version: 1},
|
||||
"qmc3": {ext: "mp3", 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}
|
||||
qmc0: { ext: 'mp3', version: 1 },
|
||||
qmc2: { ext: 'ogg', version: 1 },
|
||||
qmc3: { ext: 'mp3', 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 },
|
||||
};
|
||||
|
||||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||
@@ -56,7 +55,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||
let musicDecoded: Uint8Array | undefined;
|
||||
|
||||
if (version === 2 && globalThis.WebAssembly) {
|
||||
console.log("qmc: using wasm decoder")
|
||||
console.log('qmc: using wasm decoder');
|
||||
const v2Decrypted = await DecryptQMCWasm(fileBuffer);
|
||||
// 如果 v2 检测失败,降级到 v1 再尝试一次
|
||||
if (v2Decrypted) {
|
||||
@@ -65,28 +64,28 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||
}
|
||||
if (!musicDecoded) {
|
||||
// may throw error
|
||||
console.log("qmc: using js decoder")
|
||||
const d = new QmcDecoder(new Uint8Array(fileBuffer))
|
||||
musicDecoded = d.decrypt()
|
||||
console.log('qmc: using js decoder');
|
||||
const d = new QmcDecoder(new Uint8Array(fileBuffer));
|
||||
musicDecoded = d.decrypt();
|
||||
}
|
||||
|
||||
const ext = SniffAudioExt(musicDecoded, handler.ext);
|
||||
const mime = AudioMimeType[ext];
|
||||
|
||||
let musicBlob = new Blob([musicDecoded], {type: mime});
|
||||
let musicBlob = new Blob([musicDecoded], { type: mime });
|
||||
|
||||
const musicMeta = await metaParseBlob(musicBlob);
|
||||
for (let metaIdx in musicMeta.native) {
|
||||
if (!musicMeta.native.hasOwnProperty(metaIdx)) continue
|
||||
if (musicMeta.native[metaIdx].some(item => item.id === "TCON" && item.value === "(12)")) {
|
||||
console.warn("try using gbk encoding to decode meta")
|
||||
musicMeta.common.artist = iconv.decode(new Buffer(musicMeta.common.artist ?? ""), "gbk");
|
||||
musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ""), "gbk");
|
||||
musicMeta.common.album = iconv.decode(new Buffer(musicMeta.common.album ?? ""), "gbk");
|
||||
if (!musicMeta.native.hasOwnProperty(metaIdx)) continue;
|
||||
if (musicMeta.native[metaIdx].some((item) => item.id === 'TCON' && item.value === '(12)')) {
|
||||
console.warn('try using gbk encoding to decode meta');
|
||||
musicMeta.common.artist = iconv.decode(new Buffer(musicMeta.common.artist ?? ''), 'gbk');
|
||||
musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ''), 'gbk');
|
||||
musicMeta.common.album = iconv.decode(new Buffer(musicMeta.common.album ?? ''), 'gbk');
|
||||
}
|
||||
}
|
||||
|
||||
const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist)
|
||||
const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
|
||||
|
||||
let imgUrl = GetCoverFromFile(musicMeta);
|
||||
if (!imgUrl) {
|
||||
@@ -94,20 +93,20 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||
if (imgUrl) {
|
||||
const imageInfo = await GetImageFromURL(imgUrl);
|
||||
if (imageInfo) {
|
||||
imgUrl = imageInfo.url
|
||||
imgUrl = imageInfo.url;
|
||||
try {
|
||||
const newMeta = {picture: imageInfo.buffer, title: info.title, artists: info.artist?.split(" _ ")}
|
||||
if (ext === "mp3") {
|
||||
musicDecoded = WriteMetaToMp3(Buffer.from(musicDecoded), newMeta, musicMeta)
|
||||
musicBlob = new Blob([musicDecoded], {type: mime});
|
||||
const newMeta = { picture: imageInfo.buffer, title: info.title, artists: info.artist?.split(' _ ') };
|
||||
if (ext === 'mp3') {
|
||||
musicDecoded = WriteMetaToMp3(Buffer.from(musicDecoded), newMeta, musicMeta);
|
||||
musicBlob = new Blob([musicDecoded], { type: mime });
|
||||
} else if (ext === 'flac') {
|
||||
musicDecoded = WriteMetaToFlac(Buffer.from(musicDecoded), newMeta, musicMeta)
|
||||
musicBlob = new Blob([musicDecoded], {type: mime});
|
||||
musicDecoded = WriteMetaToFlac(Buffer.from(musicDecoded), newMeta, musicMeta);
|
||||
musicBlob = new Blob([musicDecoded], { type: mime });
|
||||
} else {
|
||||
console.info("writing metadata for " + ext + " is not being supported for now")
|
||||
console.info('writing metadata for ' + ext + ' is not being supported for now');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Error while appending cover image to file " + e)
|
||||
console.warn('Error while appending cover image to file ' + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,86 +119,83 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||
picture: imgUrl,
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
mime: mime
|
||||
}
|
||||
mime: mime,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async function getCoverImage(title: string, artist?: string, album?: string): Promise<string> {
|
||||
const song_query_url = "https://stats.ixarea.com/apis" + "/music/qq-cover"
|
||||
const song_query_url = 'https://stats.ixarea.com/apis' + '/music/qq-cover';
|
||||
try {
|
||||
const data = await queryAlbumCover(title, artist, album)
|
||||
return `${song_query_url}/${data.Type}/${data.Id}`
|
||||
const data = await queryAlbumCover(title, artist, album);
|
||||
return `${song_query_url}/${data.Type}/${data.Id}`;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
return ""
|
||||
return '';
|
||||
}
|
||||
|
||||
export class QmcDecoder {
|
||||
file: Uint8Array
|
||||
size: number
|
||||
decoded: boolean = false
|
||||
audioSize?: number
|
||||
private static readonly BYTE_COMMA = ','.charCodeAt(0)
|
||||
cipher?: QmcStreamCipher
|
||||
private static readonly BYTE_COMMA = ','.charCodeAt(0);
|
||||
file: Uint8Array;
|
||||
size: number;
|
||||
decoded: boolean = false;
|
||||
audioSize?: number;
|
||||
cipher?: QmcStreamCipher;
|
||||
|
||||
constructor(file: Uint8Array) {
|
||||
this.file = file
|
||||
this.size = file.length
|
||||
this.searchKey()
|
||||
this.file = file;
|
||||
this.size = file.length;
|
||||
this.searchKey();
|
||||
}
|
||||
|
||||
decrypt(): Uint8Array {
|
||||
if (!this.cipher) {
|
||||
throw new Error("no cipher found")
|
||||
throw new Error('no cipher found');
|
||||
}
|
||||
if (!this.audioSize || this.audioSize <= 0) {
|
||||
throw new Error("invalid audio size")
|
||||
throw new Error('invalid audio size');
|
||||
}
|
||||
const audioBuf = this.file.subarray(0, this.audioSize)
|
||||
const audioBuf = this.file.subarray(0, this.audioSize);
|
||||
|
||||
if (!this.decoded) {
|
||||
this.cipher.decrypt(audioBuf, 0)
|
||||
this.decoded = true
|
||||
this.cipher.decrypt(audioBuf, 0);
|
||||
this.decoded = true;
|
||||
}
|
||||
|
||||
return audioBuf
|
||||
return audioBuf;
|
||||
}
|
||||
|
||||
private searchKey() {
|
||||
const last4Byte = this.file.slice(-4);
|
||||
const textEnc = new TextDecoder()
|
||||
const textEnc = new TextDecoder();
|
||||
if (textEnc.decode(last4Byte) === 'QTag') {
|
||||
const sizeBuf = this.file.slice(-8, -4)
|
||||
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)
|
||||
this.setCipher(rawKey.subarray(0, keyEnd))
|
||||
const sizeBuf = this.file.slice(-8, -4);
|
||||
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);
|
||||
this.setCipher(rawKey.subarray(0, keyEnd));
|
||||
} else {
|
||||
const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset);
|
||||
const keySize = sizeView.getUint32(0, true)
|
||||
const keySize = sizeView.getUint32(0, true);
|
||||
if (keySize < 0x300) {
|
||||
this.audioSize = this.size - keySize - 4
|
||||
const rawKey = this.file.subarray(this.audioSize, this.size - 4)
|
||||
this.setCipher(rawKey)
|
||||
this.audioSize = this.size - keySize - 4;
|
||||
const rawKey = this.file.subarray(this.audioSize, this.size - 4);
|
||||
this.setCipher(rawKey);
|
||||
} else {
|
||||
this.audioSize = this.size
|
||||
this.cipher = new QmcStaticCipher()
|
||||
this.audioSize = this.size;
|
||||
this.cipher = new QmcStaticCipher();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setCipher(keyRaw: Uint8Array) {
|
||||
const keyDec = QmcDeriveKey(keyRaw)
|
||||
const keyDec = QmcDeriveKey(keyRaw);
|
||||
if (keyDec.length > 300) {
|
||||
this.cipher = new QmcRC4Cipher(keyDec)
|
||||
this.cipher = new QmcRC4Cipher(keyDec);
|
||||
} else {
|
||||
this.cipher = new QmcMapCipher(keyDec)
|
||||
this.cipher = new QmcMapCipher(keyDec);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,115 +1,117 @@
|
||||
import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher} from "@/decrypt/qmc_cipher";
|
||||
import fs from 'fs'
|
||||
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher } from '@/decrypt/qmc_cipher';
|
||||
import fs from 'fs';
|
||||
|
||||
test("static cipher [0x7ff8,0x8000) ", () => {
|
||||
test('static cipher [0x7ff8,0x8000) ', () => {
|
||||
//prettier-ignore
|
||||
const expected = new Uint8Array([
|
||||
0xD8, 0x52, 0xF7, 0x67, 0x90, 0xCA, 0xD6, 0x4A,
|
||||
0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52, 0xD8,
|
||||
])
|
||||
|
||||
const c = new QmcStaticCipher()
|
||||
const buf = new Uint8Array(16)
|
||||
c.decrypt(buf, 0x7ff8)
|
||||
const c = new QmcStaticCipher();
|
||||
const buf = new Uint8Array(16);
|
||||
c.decrypt(buf, 0x7ff8);
|
||||
|
||||
expect(buf).toStrictEqual(expected)
|
||||
})
|
||||
expect(buf).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
test("static cipher [0,0x10) ", () => {
|
||||
test('static cipher [0,0x10) ', () => {
|
||||
//prettier-ignore
|
||||
const expected = new Uint8Array([
|
||||
0xC3, 0x4A, 0xD6, 0xCA, 0x90, 0x67, 0xF7, 0x52,
|
||||
0xD8, 0xA1, 0x66, 0x62, 0x9F, 0x5B, 0x09, 0x00,
|
||||
])
|
||||
|
||||
const c = new QmcStaticCipher()
|
||||
const buf = new Uint8Array(16)
|
||||
c.decrypt(buf, 0)
|
||||
const c = new QmcStaticCipher();
|
||||
const buf = new Uint8Array(16);
|
||||
c.decrypt(buf, 0);
|
||||
|
||||
expect(buf).toStrictEqual(expected)
|
||||
})
|
||||
expect(buf).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
|
||||
test("map cipher: get mask", () => {
|
||||
test('map cipher: get mask', () => {
|
||||
//prettier-ignore
|
||||
const expected = new Uint8Array([
|
||||
0xBB, 0x7D, 0x80, 0xBE, 0xFF, 0x38, 0x81, 0xFB,
|
||||
0xBB, 0xFF, 0x82, 0x3C, 0xFF, 0xBA, 0x83, 0x79,
|
||||
])
|
||||
const key = new Uint8Array(256)
|
||||
for (let i = 0; i < 256; i++) key[i] = i
|
||||
const buf = new Uint8Array(16)
|
||||
const key = new Uint8Array(256);
|
||||
for (let i = 0; i < 256; i++) key[i] = i;
|
||||
const buf = new Uint8Array(16);
|
||||
|
||||
const c = new QmcMapCipher(key)
|
||||
c.decrypt(buf, 0)
|
||||
expect(buf).toStrictEqual(expected)
|
||||
})
|
||||
const c = new QmcMapCipher(key);
|
||||
c.decrypt(buf, 0);
|
||||
expect(buf).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
function loadTestDataCipher(name: string): {
|
||||
key: Uint8Array,
|
||||
cipherText: Uint8Array,
|
||||
clearText: Uint8Array
|
||||
key: Uint8Array;
|
||||
cipherText: Uint8Array;
|
||||
clearText: Uint8Array;
|
||||
} {
|
||||
return {
|
||||
key: fs.readFileSync(`testdata/${name}_key.bin`),
|
||||
cipherText: fs.readFileSync(`testdata/${name}_raw.bin`),
|
||||
clearText: fs.readFileSync(`testdata/${name}_target.bin`)
|
||||
}
|
||||
clearText: fs.readFileSync(`testdata/${name}_target.bin`),
|
||||
};
|
||||
}
|
||||
|
||||
test("map cipher: real file", async () => {
|
||||
const cases = ["mflac_map", "mgg_map"]
|
||||
test('map cipher: real file', async () => {
|
||||
const cases = ['mflac_map', 'mgg_map'];
|
||||
for (const name of cases) {
|
||||
const {key, clearText, cipherText} = loadTestDataCipher(name)
|
||||
const c = new QmcMapCipher(key)
|
||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
||||
const c = new QmcMapCipher(key);
|
||||
|
||||
c.decrypt(cipherText, 0)
|
||||
c.decrypt(cipherText, 0);
|
||||
|
||||
expect(cipherText).toStrictEqual(clearText)
|
||||
expect(cipherText).toStrictEqual(clearText);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
test("rc4 cipher: real file", async () => {
|
||||
const cases = ["mflac0_rc4"]
|
||||
test('rc4 cipher: real file', async () => {
|
||||
const cases = ['mflac0_rc4'];
|
||||
for (const name of cases) {
|
||||
const {key, clearText, cipherText} = loadTestDataCipher(name)
|
||||
const c = new QmcRC4Cipher(key)
|
||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
||||
const c = new QmcRC4Cipher(key);
|
||||
|
||||
c.decrypt(cipherText, 0)
|
||||
c.decrypt(cipherText, 0);
|
||||
|
||||
expect(cipherText).toStrictEqual(clearText)
|
||||
expect(cipherText).toStrictEqual(clearText);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
test("rc4 cipher: first segment", async () => {
|
||||
const cases = ["mflac0_rc4"]
|
||||
test('rc4 cipher: first segment', async () => {
|
||||
const cases = ['mflac0_rc4'];
|
||||
for (const name of cases) {
|
||||
const {key, clearText, cipherText} = loadTestDataCipher(name)
|
||||
const c = new QmcRC4Cipher(key)
|
||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
||||
const c = new QmcRC4Cipher(key);
|
||||
|
||||
const buf = cipherText.slice(0, 128)
|
||||
c.decrypt(buf, 0)
|
||||
expect(buf).toStrictEqual(clearText.slice(0, 128))
|
||||
const buf = cipherText.slice(0, 128);
|
||||
c.decrypt(buf, 0);
|
||||
expect(buf).toStrictEqual(clearText.slice(0, 128));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
test("rc4 cipher: align block (128~5120)", async () => {
|
||||
const cases = ["mflac0_rc4"]
|
||||
test('rc4 cipher: align block (128~5120)', async () => {
|
||||
const cases = ['mflac0_rc4'];
|
||||
for (const name of cases) {
|
||||
const {key, clearText, cipherText} = loadTestDataCipher(name)
|
||||
const c = new QmcRC4Cipher(key)
|
||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
||||
const c = new QmcRC4Cipher(key);
|
||||
|
||||
const buf = cipherText.slice(128, 5120)
|
||||
c.decrypt(buf, 128)
|
||||
expect(buf).toStrictEqual(clearText.slice(128, 5120))
|
||||
const buf = cipherText.slice(128, 5120);
|
||||
c.decrypt(buf, 128);
|
||||
expect(buf).toStrictEqual(clearText.slice(128, 5120));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
test("rc4 cipher: simple block (5120~10240)", async () => {
|
||||
const cases = ["mflac0_rc4"]
|
||||
test('rc4 cipher: simple block (5120~10240)', async () => {
|
||||
const cases = ['mflac0_rc4'];
|
||||
for (const name of cases) {
|
||||
const {key, clearText, cipherText} = loadTestDataCipher(name)
|
||||
const c = new QmcRC4Cipher(key)
|
||||
const { key, clearText, cipherText } = loadTestDataCipher(name);
|
||||
const c = new QmcRC4Cipher(key);
|
||||
|
||||
const buf = cipherText.slice(5120, 10240)
|
||||
c.decrypt(buf, 5120)
|
||||
expect(buf).toStrictEqual(clearText.slice(5120, 10240))
|
||||
const buf = cipherText.slice(5120, 10240);
|
||||
c.decrypt(buf, 5120);
|
||||
expect(buf).toStrictEqual(clearText.slice(5120, 10240));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@@ -1,9 +1,9 @@
|
||||
export interface QmcStreamCipher {
|
||||
decrypt(buf: Uint8Array, offset: number): void
|
||||
decrypt(buf: Uint8Array, offset: number): void;
|
||||
}
|
||||
|
||||
|
||||
export class QmcStaticCipher implements QmcStreamCipher {
|
||||
//prettier-ignore
|
||||
private static readonly staticCipherBox: Uint8Array = new Uint8Array([
|
||||
0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00
|
||||
0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08
|
||||
@@ -40,26 +40,26 @@ export class QmcStaticCipher implements QmcStreamCipher {
|
||||
])
|
||||
|
||||
public getMask(offset: number) {
|
||||
if (offset > 0x7FFF) offset %= 0x7FFF
|
||||
return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff]
|
||||
if (offset > 0x7fff) offset %= 0x7fff;
|
||||
return QmcStaticCipher.staticCipherBox[(offset * offset + 27) & 0xff];
|
||||
}
|
||||
|
||||
public decrypt(buf: Uint8Array, offset: number) {
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
buf[i] ^= this.getMask(offset + i)
|
||||
buf[i] ^= this.getMask(offset + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class QmcMapCipher implements QmcStreamCipher {
|
||||
key: Uint8Array
|
||||
n: number
|
||||
key: Uint8Array;
|
||||
n: number;
|
||||
|
||||
constructor(key: Uint8Array) {
|
||||
if (key.length == 0) throw Error("qmc/cipher_map: invalid key size")
|
||||
if (key.length == 0) throw Error('qmc/cipher_map: invalid key size');
|
||||
|
||||
this.key = key
|
||||
this.n = key.length
|
||||
this.key = key;
|
||||
this.n = key.length;
|
||||
}
|
||||
|
||||
private static rotate(value: number, bits: number) {
|
||||
@@ -71,7 +71,7 @@ export class QmcMapCipher implements QmcStreamCipher {
|
||||
|
||||
decrypt(buf: Uint8Array, offset: number): void {
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
buf[i] ^= this.getMask(offset + i)
|
||||
buf[i] ^= this.getMask(offset + i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,27 +79,26 @@ export class QmcMapCipher implements QmcStreamCipher {
|
||||
if (offset > 0x7fff) offset %= 0x7fff;
|
||||
|
||||
const idx = (offset * offset + 71214) % this.n;
|
||||
return QmcMapCipher.rotate(this.key[idx], idx & 0x7)
|
||||
return QmcMapCipher.rotate(this.key[idx], idx & 0x7);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class QmcRC4Cipher implements QmcStreamCipher {
|
||||
private static readonly FIRST_SEGMENT_SIZE = 0x80;
|
||||
private static readonly SEGMENT_SIZE = 5120
|
||||
private static readonly SEGMENT_SIZE = 5120;
|
||||
|
||||
S: Uint8Array
|
||||
N: number
|
||||
key: Uint8Array
|
||||
hash: number
|
||||
S: Uint8Array;
|
||||
N: number;
|
||||
key: Uint8Array;
|
||||
hash: number;
|
||||
|
||||
constructor(key: Uint8Array) {
|
||||
if (key.length == 0) {
|
||||
throw Error("invalid key size")
|
||||
throw Error('invalid key size');
|
||||
}
|
||||
|
||||
this.key = key
|
||||
this.N = key.length
|
||||
this.key = key;
|
||||
this.N = key.length;
|
||||
|
||||
// init seed box
|
||||
this.S = new Uint8Array(this.N);
|
||||
@@ -109,7 +108,7 @@ export class QmcRC4Cipher implements QmcStreamCipher {
|
||||
let j = 0;
|
||||
for (let i = 0; i < this.N; ++i) {
|
||||
j = (this.S[i] + j + this.key[i % this.N]) % this.N;
|
||||
[this.S[i], this.S[j]] = [this.S[j], this.S[i]]
|
||||
[this.S[i], this.S[j]] = [this.S[j], this.S[i]];
|
||||
}
|
||||
|
||||
// init hash base
|
||||
@@ -125,7 +124,6 @@ export class QmcRC4Cipher implements QmcStreamCipher {
|
||||
|
||||
this.hash = next_hash;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
decrypt(buf: Uint8Array, offset: number): void {
|
||||
@@ -133,52 +131,50 @@ export class QmcRC4Cipher implements QmcStreamCipher {
|
||||
let processed = 0;
|
||||
const postProcess = (len: number): boolean => {
|
||||
toProcess -= len;
|
||||
processed += len
|
||||
offset += len
|
||||
return toProcess == 0
|
||||
}
|
||||
processed += len;
|
||||
offset += len;
|
||||
return toProcess == 0;
|
||||
};
|
||||
|
||||
// Initial segment
|
||||
if (offset < QmcRC4Cipher.FIRST_SEGMENT_SIZE) {
|
||||
const len_segment = Math.min(buf.length, QmcRC4Cipher.FIRST_SEGMENT_SIZE - offset);
|
||||
this.encFirstSegment(buf.subarray(0, len_segment), offset);
|
||||
if (postProcess(len_segment)) return
|
||||
if (postProcess(len_segment)) return;
|
||||
}
|
||||
|
||||
// align segment
|
||||
if (offset % QmcRC4Cipher.SEGMENT_SIZE != 0) {
|
||||
const len_segment = Math.min(QmcRC4Cipher.SEGMENT_SIZE - (offset % QmcRC4Cipher.SEGMENT_SIZE), toProcess);
|
||||
this.encASegment(buf.subarray(processed, processed + len_segment), offset);
|
||||
if (postProcess(len_segment)) return
|
||||
if (postProcess(len_segment)) return;
|
||||
}
|
||||
|
||||
// Batch process segments
|
||||
while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) {
|
||||
this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset);
|
||||
postProcess(QmcRC4Cipher.SEGMENT_SIZE)
|
||||
postProcess(QmcRC4Cipher.SEGMENT_SIZE);
|
||||
}
|
||||
|
||||
// Last segment (incomplete segment)
|
||||
if (toProcess > 0) {
|
||||
this.encASegment(buf.subarray(processed), offset);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private encFirstSegment(buf: Uint8Array, offset: number) {
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
|
||||
buf[i] ^= this.key[this.getSegmentKey(offset + i)];
|
||||
}
|
||||
}
|
||||
|
||||
private encASegment(buf: Uint8Array, offset: number) {
|
||||
// Initialise a new seed box
|
||||
const S = this.S.slice(0)
|
||||
const S = this.S.slice(0);
|
||||
|
||||
// Calculate the number of bytes to skip.
|
||||
// The initial "key" derived from segment id, plus the current offset.
|
||||
const skipLen = (offset % QmcRC4Cipher.SEGMENT_SIZE) + this.getSegmentKey(offset / QmcRC4Cipher.SEGMENT_SIZE)
|
||||
const skipLen = (offset % QmcRC4Cipher.SEGMENT_SIZE) + this.getSegmentKey(offset / QmcRC4Cipher.SEGMENT_SIZE);
|
||||
|
||||
// decrypt the block
|
||||
let j = 0;
|
||||
@@ -186,7 +182,7 @@ export class QmcRC4Cipher implements QmcStreamCipher {
|
||||
for (let i = -skipLen; i < buf.length; i++) {
|
||||
j = (j + 1) % this.N;
|
||||
k = (S[j] + k) % this.N;
|
||||
[S[k], S[j]] = [S[j], S[k]]
|
||||
[S[k], S[j]] = [S[j], S[k]];
|
||||
|
||||
if (i >= 0) {
|
||||
buf[i] ^= S[(S[j] + S[k]) % this.N];
|
||||
@@ -195,8 +191,8 @@ export class QmcRC4Cipher implements QmcStreamCipher {
|
||||
}
|
||||
|
||||
private getSegmentKey(id: number): number {
|
||||
const seed = this.key[id % this.N]
|
||||
const idx = (this.hash / ((id + 1) * seed) * 100.0) | 0;
|
||||
return idx % this.N
|
||||
const seed = this.key[id % this.N];
|
||||
const idx = ((this.hash / ((id + 1) * seed)) * 100.0) | 0;
|
||||
return idx % this.N;
|
||||
}
|
||||
}
|
||||
|
@@ -1,30 +1,26 @@
|
||||
import {QmcDeriveKey, simpleMakeKey} from "@/decrypt/qmc_key";
|
||||
import fs from "fs";
|
||||
import { QmcDeriveKey, simpleMakeKey } from '@/decrypt/qmc_key';
|
||||
import fs from 'fs';
|
||||
|
||||
test("key dec: make simple key", () => {
|
||||
expect(
|
||||
simpleMakeKey(106, 8)
|
||||
).toStrictEqual(
|
||||
[0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b]
|
||||
)
|
||||
})
|
||||
test('key dec: make simple key', () => {
|
||||
expect(simpleMakeKey(106, 8)).toStrictEqual([0x69, 0x56, 0x46, 0x38, 0x2b, 0x20, 0x15, 0x0b]);
|
||||
});
|
||||
|
||||
function loadTestDataKeyDecrypt(name: string): {
|
||||
cipherText: Uint8Array,
|
||||
clearText: Uint8Array
|
||||
cipherText: Uint8Array;
|
||||
clearText: Uint8Array;
|
||||
} {
|
||||
return {
|
||||
cipherText: fs.readFileSync(`testdata/${name}_key_raw.bin`),
|
||||
clearText: fs.readFileSync(`testdata/${name}_key.bin`)
|
||||
}
|
||||
clearText: fs.readFileSync(`testdata/${name}_key.bin`),
|
||||
};
|
||||
}
|
||||
|
||||
test("key dec: real file", async () => {
|
||||
const cases = ["mflac_map", "mgg_map", "mflac0_rc4"]
|
||||
test('key dec: real file', async () => {
|
||||
const cases = ['mflac_map', 'mgg_map', 'mflac0_rc4'];
|
||||
for (const name of cases) {
|
||||
const {clearText, cipherText} = loadTestDataKeyDecrypt(name)
|
||||
const buf = QmcDeriveKey(cipherText)
|
||||
const { clearText, cipherText } = loadTestDataKeyDecrypt(name);
|
||||
const buf = QmcDeriveKey(cipherText);
|
||||
|
||||
expect(buf).toStrictEqual(clearText)
|
||||
expect(buf).toStrictEqual(clearText);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@@ -1,86 +1,83 @@
|
||||
import {TeaCipher} from "@/utils/tea";
|
||||
import { TeaCipher } from '@/utils/tea';
|
||||
|
||||
const SALT_LEN = 2
|
||||
const ZERO_LEN = 7
|
||||
const SALT_LEN = 2;
|
||||
const ZERO_LEN = 7;
|
||||
|
||||
export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
|
||||
const textDec = new TextDecoder()
|
||||
const rawDec = Buffer.from(textDec.decode(raw), 'base64')
|
||||
const textDec = new TextDecoder();
|
||||
const rawDec = Buffer.from(textDec.decode(raw), 'base64');
|
||||
let n = rawDec.length;
|
||||
if (n < 16) {
|
||||
throw Error("key length is too short")
|
||||
throw Error('key length is too short');
|
||||
}
|
||||
|
||||
const simpleKey = simpleMakeKey(106, 8)
|
||||
const simpleKey = simpleMakeKey(106, 8);
|
||||
let teaKey = new Uint8Array(16);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
teaKey[i << 1] = simpleKey[i];
|
||||
teaKey[(i << 1) + 1] = rawDec[i];
|
||||
}
|
||||
const sub = decryptTencentTea(rawDec.subarray(8), teaKey)
|
||||
rawDec.set(sub, 8)
|
||||
return rawDec.subarray(0, 8 + sub.length)
|
||||
|
||||
const sub = decryptTencentTea(rawDec.subarray(8), teaKey);
|
||||
rawDec.set(sub, 8);
|
||||
return rawDec.subarray(0, 8 + sub.length);
|
||||
}
|
||||
|
||||
// simpleMakeKey exported only for unit test
|
||||
export function simpleMakeKey(salt: number, length: number): number[] {
|
||||
const keyBuf: number[] = []
|
||||
const keyBuf: number[] = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
const tmp = Math.tan(salt + i * 0.1)
|
||||
keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0)
|
||||
const tmp = Math.tan(salt + i * 0.1);
|
||||
keyBuf[i] = 0xff & (Math.abs(tmp) * 100.0);
|
||||
}
|
||||
return keyBuf
|
||||
return keyBuf;
|
||||
}
|
||||
|
||||
|
||||
function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
|
||||
if (inBuf.length % 8 != 0) {
|
||||
throw Error("inBuf size not a multiple of the block size")
|
||||
throw Error('inBuf size not a multiple of the block size');
|
||||
}
|
||||
if (inBuf.length < 16) {
|
||||
throw Error("inBuf size too small")
|
||||
throw Error('inBuf size too small');
|
||||
}
|
||||
|
||||
const blk = new TeaCipher(key, 32)
|
||||
const blk = new TeaCipher(key, 32);
|
||||
|
||||
const tmpBuf = new Uint8Array(8);
|
||||
const tmpView = new DataView(tmpBuf.buffer);
|
||||
|
||||
blk.decrypt(tmpView, new DataView(inBuf.buffer, inBuf.byteOffset, 8))
|
||||
blk.decrypt(tmpView, new DataView(inBuf.buffer, inBuf.byteOffset, 8));
|
||||
|
||||
const nPadLen = tmpBuf[0] & 0x7;//只要最低三位
|
||||
const nPadLen = tmpBuf[0] & 0x7; //只要最低三位
|
||||
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
|
||||
const outLen = inBuf.length - 1 /*PadLen*/ - nPadLen - SALT_LEN - ZERO_LEN;
|
||||
const outBuf = new Uint8Array(outLen)
|
||||
const outBuf = new Uint8Array(outLen);
|
||||
|
||||
let ivPrev = new Uint8Array(8);
|
||||
let ivCur = inBuf.slice(0, 8); // init iv
|
||||
let inBufPos = 8;
|
||||
|
||||
|
||||
// 跳过 Padding Len 和 Padding
|
||||
let tmpIdx = 1 + nPadLen;
|
||||
|
||||
// CBC IV 处理
|
||||
const cryptBlock = () => {
|
||||
ivPrev = ivCur;
|
||||
ivCur = inBuf.slice(inBufPos, inBufPos + 8)
|
||||
ivCur = inBuf.slice(inBufPos, inBufPos + 8);
|
||||
for (let j = 0; j < 8; j++) {
|
||||
tmpBuf[j] ^= ivCur[j]
|
||||
tmpBuf[j] ^= ivCur[j];
|
||||
}
|
||||
blk.decrypt(tmpView, tmpView)
|
||||
blk.decrypt(tmpView, tmpView);
|
||||
inBufPos += 8;
|
||||
tmpIdx = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 跳过 Salt
|
||||
for (let i = 1; i <= SALT_LEN;) {
|
||||
for (let i = 1; i <= SALT_LEN; ) {
|
||||
if (tmpIdx < 8) {
|
||||
tmpIdx++;
|
||||
i++;
|
||||
} else {
|
||||
cryptBlock()
|
||||
cryptBlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,19 +86,18 @@ function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
|
||||
while (outBufPos < outLen) {
|
||||
if (tmpIdx < 8) {
|
||||
outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx];
|
||||
outBufPos++
|
||||
outBufPos++;
|
||||
tmpIdx++;
|
||||
} else {
|
||||
cryptBlock()
|
||||
cryptBlock();
|
||||
}
|
||||
}
|
||||
|
||||
// 校验Zero
|
||||
for (let i = 1; i <= ZERO_LEN; i++) {
|
||||
if (tmpBuf[tmpIdx] != ivPrev[tmpIdx]) {
|
||||
throw Error("zero check failed")
|
||||
throw Error('zero check failed');
|
||||
}
|
||||
}
|
||||
return outBuf
|
||||
return outBuf;
|
||||
}
|
||||
|
||||
|
@@ -8,13 +8,13 @@ const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
function MergeUint8Array(array: Uint8Array[]): Uint8Array {
|
||||
let length = 0;
|
||||
array.forEach(item => {
|
||||
array.forEach((item) => {
|
||||
length += item.length;
|
||||
});
|
||||
|
||||
let mergedArray = new Uint8Array(length);
|
||||
let offset = 0;
|
||||
array.forEach(item => {
|
||||
array.forEach((item) => {
|
||||
mergedArray.set(item, offset);
|
||||
offset += item.length;
|
||||
});
|
||||
@@ -42,16 +42,12 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
|
||||
const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection());
|
||||
|
||||
// 进行检测
|
||||
const detectOK = QMCCrypto.detectKeyEndPosition(
|
||||
pDetectionResult,
|
||||
pDetectionBuf,
|
||||
detectionBuf.length
|
||||
);
|
||||
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 position = QMCCrypto.getValue(pDetectionResult, 'i32');
|
||||
const len = QMCCrypto.getValue(pDetectionResult + 4, 'i32');
|
||||
|
||||
// 释放内存
|
||||
QMCCrypto._free(pDetectionBuf);
|
||||
@@ -66,9 +62,7 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
|
||||
const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position;
|
||||
|
||||
// 提取嵌入到文件的 EKey
|
||||
const ekey = new Uint8Array(
|
||||
mggBlob.slice(decryptedSize, decryptedSize + len)
|
||||
);
|
||||
const ekey = new Uint8Array(mggBlob.slice(decryptedSize, decryptedSize + len));
|
||||
|
||||
// 解码 UTF-8 数据到 string
|
||||
const decoder = new TextDecoder();
|
||||
@@ -85,9 +79,7 @@ export async function DecryptQMCWasm(mggBlob: ArrayBuffer) {
|
||||
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
|
||||
|
||||
// 解密一些片段
|
||||
const blockData = new Uint8Array(
|
||||
mggBlob.slice(offset, offset + blockSize)
|
||||
);
|
||||
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));
|
||||
|
@@ -1,51 +1,50 @@
|
||||
import {
|
||||
AudioMimeType,
|
||||
GetArrayBuffer,
|
||||
GetCoverFromFile,
|
||||
GetMetaFromFile,
|
||||
SniffAudioExt,
|
||||
SplitFilename
|
||||
} from "@/decrypt/utils";
|
||||
AudioMimeType,
|
||||
GetArrayBuffer,
|
||||
GetCoverFromFile,
|
||||
GetMetaFromFile,
|
||||
SniffAudioExt,
|
||||
SplitFilename,
|
||||
} from '@/decrypt/utils';
|
||||
|
||||
import {Decrypt as QmcDecrypt, HandlerMap} from "@/decrypt/qmc";
|
||||
import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc';
|
||||
|
||||
import {DecryptResult} from "@/decrypt/entity";
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
|
||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||
|
||||
export async function Decrypt(file: Blob, raw_filename: string, _: string)
|
||||
: Promise<DecryptResult> {
|
||||
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
||||
let length = buffer.length
|
||||
for (let i = 0; i < length; i++) {
|
||||
buffer[i] ^= 0xf4
|
||||
if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4;
|
||||
else if (buffer[i] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1;
|
||||
else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2;
|
||||
else buffer[i] = (buffer[i] - 0xc0) * 4 + 3;
|
||||
}
|
||||
let ext = SniffAudioExt(buffer, "");
|
||||
const newName = SplitFilename(raw_filename)
|
||||
let audioBlob: Blob
|
||||
if (ext !== "" || newName.ext === "mp3") {
|
||||
audioBlob = new Blob([buffer], {type: AudioMimeType[ext]})
|
||||
} else if (newName.ext in HandlerMap) {
|
||||
audioBlob = new Blob([buffer], {type: "application/octet-stream"})
|
||||
return QmcDecrypt(audioBlob, newName.name, newName.ext);
|
||||
} else {
|
||||
throw "不支持的QQ音乐缓存格式"
|
||||
}
|
||||
const tag = await metaParseBlob(audioBlob);
|
||||
const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
|
||||
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
|
||||
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
||||
let length = buffer.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
buffer[i] ^= 0xf4;
|
||||
if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4;
|
||||
else if (buffer[i] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1;
|
||||
else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2;
|
||||
else buffer[i] = (buffer[i] - 0xc0) * 4 + 3;
|
||||
}
|
||||
let ext = SniffAudioExt(buffer, '');
|
||||
const newName = SplitFilename(raw_filename);
|
||||
let audioBlob: Blob;
|
||||
if (ext !== '' || newName.ext === 'mp3') {
|
||||
audioBlob = new Blob([buffer], { type: AudioMimeType[ext] });
|
||||
} else if (newName.ext in HandlerMap) {
|
||||
audioBlob = new Blob([buffer], { type: 'application/octet-stream' });
|
||||
return QmcDecrypt(audioBlob, newName.name, newName.ext);
|
||||
} else {
|
||||
throw '不支持的QQ音乐缓存格式';
|
||||
}
|
||||
const tag = await metaParseBlob(audioBlob);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
|
||||
|
||||
return {
|
||||
title,
|
||||
artist,
|
||||
ext,
|
||||
album: tag.common.album,
|
||||
picture: GetCoverFromFile(tag),
|
||||
file: URL.createObjectURL(audioBlob),
|
||||
blob: audioBlob,
|
||||
mime: AudioMimeType[ext]
|
||||
}
|
||||
return {
|
||||
title,
|
||||
artist,
|
||||
ext,
|
||||
album: tag.common.album,
|
||||
picture: GetCoverFromFile(tag),
|
||||
file: URL.createObjectURL(audioBlob),
|
||||
blob: audioBlob,
|
||||
mime: AudioMimeType[ext],
|
||||
};
|
||||
}
|
||||
|
@@ -1,28 +1,32 @@
|
||||
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils";
|
||||
import { AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt } from '@/decrypt/utils';
|
||||
|
||||
import {DecryptResult} from "@/decrypt/entity";
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
|
||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||
|
||||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string, detect: boolean = true)
|
||||
: Promise<DecryptResult> {
|
||||
let ext = raw_ext;
|
||||
if (detect) {
|
||||
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
||||
ext = SniffAudioExt(buffer, raw_ext);
|
||||
if (ext !== raw_ext) file = new Blob([buffer], {type: AudioMimeType[ext]})
|
||||
}
|
||||
const tag = await metaParseBlob(file);
|
||||
const {title, artist} = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist)
|
||||
export async function Decrypt(
|
||||
file: Blob,
|
||||
raw_filename: string,
|
||||
raw_ext: string,
|
||||
detect: boolean = true,
|
||||
): Promise<DecryptResult> {
|
||||
let ext = raw_ext;
|
||||
if (detect) {
|
||||
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
||||
ext = SniffAudioExt(buffer, raw_ext);
|
||||
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
||||
}
|
||||
const tag = await metaParseBlob(file);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
|
||||
|
||||
return {
|
||||
title,
|
||||
artist,
|
||||
ext,
|
||||
album: tag.common.album,
|
||||
picture: GetCoverFromFile(tag),
|
||||
file: URL.createObjectURL(file),
|
||||
blob: file,
|
||||
mime: AudioMimeType[ext]
|
||||
}
|
||||
return {
|
||||
title,
|
||||
artist,
|
||||
ext,
|
||||
album: tag.common.album,
|
||||
picture: GetCoverFromFile(tag),
|
||||
file: URL.createObjectURL(file),
|
||||
blob: file,
|
||||
mime: AudioMimeType[ext],
|
||||
};
|
||||
}
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import {Decrypt as RawDecrypt} from "./raw";
|
||||
import {GetArrayBuffer} from "@/decrypt/utils";
|
||||
import {DecryptResult} from "@/decrypt/entity";
|
||||
import { Decrypt as RawDecrypt } from './raw';
|
||||
import { GetArrayBuffer } from '@/decrypt/utils';
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
|
||||
const TM_HEADER = [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70];
|
||||
|
||||
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
|
||||
const audioData = new Uint8Array(await GetArrayBuffer(file));
|
||||
for (let cur = 0; cur < 8; ++cur) {
|
||||
audioData[cur] = TM_HEADER[cur];
|
||||
}
|
||||
const musicData = new Blob([audioData], {type: "audio/mp4"});
|
||||
return await RawDecrypt(musicData, raw_filename, "m4a", false)
|
||||
const audioData = new Uint8Array(await GetArrayBuffer(file));
|
||||
for (let cur = 0; cur < 8; ++cur) {
|
||||
audioData[cur] = TM_HEADER[cur];
|
||||
}
|
||||
const musicData = new Blob([audioData], { type: 'audio/mp4' });
|
||||
return await RawDecrypt(musicData, raw_filename, 'm4a', false);
|
||||
}
|
||||
|
@@ -1,177 +1,176 @@
|
||||
import {IAudioMetadata} from "music-metadata-browser";
|
||||
import ID3Writer from "browser-id3-writer";
|
||||
import MetaFlac from "metaflac-js";
|
||||
import { IAudioMetadata } from 'music-metadata-browser';
|
||||
import ID3Writer from 'browser-id3-writer';
|
||||
import MetaFlac from 'metaflac-js';
|
||||
|
||||
export const FLAC_HEADER = [0x66, 0x4C, 0x61, 0x43];
|
||||
export const FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43];
|
||||
export const MP3_HEADER = [0x49, 0x44, 0x33];
|
||||
export const OGG_HEADER = [0x4F, 0x67, 0x67, 0x53];
|
||||
export const OGG_HEADER = [0x4f, 0x67, 0x67, 0x53];
|
||||
export const M4A_HEADER = [0x66, 0x74, 0x79, 0x70];
|
||||
export const WMA_HEADER = [
|
||||
0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11,
|
||||
0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C,
|
||||
]
|
||||
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46]
|
||||
export const AAC_HEADER = [0xFF, 0xF1]
|
||||
export const DFF_HEADER = [0x46, 0x52, 0x4D, 0x38]
|
||||
0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c,
|
||||
];
|
||||
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46];
|
||||
export const AAC_HEADER = [0xff, 0xf1];
|
||||
export const DFF_HEADER = [0x46, 0x52, 0x4d, 0x38];
|
||||
|
||||
export const AudioMimeType: { [key: string]: string } = {
|
||||
mp3: "audio/mpeg",
|
||||
flac: "audio/flac",
|
||||
m4a: "audio/mp4",
|
||||
ogg: "audio/ogg",
|
||||
wma: "audio/x-ms-wma",
|
||||
wav: "audio/x-wav",
|
||||
dff: "audio/x-dff"
|
||||
mp3: 'audio/mpeg',
|
||||
flac: 'audio/flac',
|
||||
m4a: 'audio/mp4',
|
||||
ogg: 'audio/ogg',
|
||||
wma: 'audio/x-ms-wma',
|
||||
wav: 'audio/x-wav',
|
||||
dff: 'audio/x-dff',
|
||||
};
|
||||
|
||||
|
||||
export function BytesHasPrefix(data: Uint8Array, prefix: number[]): boolean {
|
||||
if (prefix.length > data.length) return false
|
||||
return prefix.every((val, idx) => {
|
||||
return val === data[idx];
|
||||
})
|
||||
if (prefix.length > data.length) return false;
|
||||
return prefix.every((val, idx) => {
|
||||
return val === data[idx];
|
||||
});
|
||||
}
|
||||
|
||||
export function BytesEqual(a: Uint8Array, b: Uint8Array,): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((val, idx) => {
|
||||
return val === b[idx];
|
||||
})
|
||||
export function BytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((val, idx) => {
|
||||
return val === b[idx];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function SniffAudioExt(data: Uint8Array, fallback_ext: string = "mp3"): string {
|
||||
if (BytesHasPrefix(data, MP3_HEADER)) return "mp3"
|
||||
if (BytesHasPrefix(data, FLAC_HEADER)) return "flac"
|
||||
if (BytesHasPrefix(data, OGG_HEADER)) return "ogg"
|
||||
if (data.length >= 4 + M4A_HEADER.length &&
|
||||
BytesHasPrefix(data.slice(4), M4A_HEADER)) return "m4a"
|
||||
if (BytesHasPrefix(data, WAV_HEADER)) return "wav"
|
||||
if (BytesHasPrefix(data, WMA_HEADER)) return "wma"
|
||||
if (BytesHasPrefix(data, AAC_HEADER)) return "aac"
|
||||
if (BytesHasPrefix(data, DFF_HEADER)) return "dff"
|
||||
return fallback_ext;
|
||||
export function SniffAudioExt(data: Uint8Array, fallback_ext: string = 'mp3'): string {
|
||||
if (BytesHasPrefix(data, MP3_HEADER)) return 'mp3';
|
||||
if (BytesHasPrefix(data, FLAC_HEADER)) return 'flac';
|
||||
if (BytesHasPrefix(data, OGG_HEADER)) return 'ogg';
|
||||
if (data.length >= 4 + M4A_HEADER.length && BytesHasPrefix(data.slice(4), M4A_HEADER)) return 'm4a';
|
||||
if (BytesHasPrefix(data, WAV_HEADER)) return 'wav';
|
||||
if (BytesHasPrefix(data, WMA_HEADER)) return 'wma';
|
||||
if (BytesHasPrefix(data, AAC_HEADER)) return 'aac';
|
||||
if (BytesHasPrefix(data, DFF_HEADER)) return 'dff';
|
||||
return fallback_ext;
|
||||
}
|
||||
|
||||
export function GetArrayBuffer(obj: Blob): Promise<ArrayBuffer> {
|
||||
if (!!obj.arrayBuffer) return obj.arrayBuffer()
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const rs = e.target?.result
|
||||
if (!rs) {
|
||||
reject("read file failed")
|
||||
} else {
|
||||
resolve(rs as ArrayBuffer)
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(obj);
|
||||
});
|
||||
if (!!obj.arrayBuffer) return obj.arrayBuffer();
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const rs = e.target?.result;
|
||||
if (!rs) {
|
||||
reject('read file failed');
|
||||
} else {
|
||||
resolve(rs as ArrayBuffer);
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(obj);
|
||||
});
|
||||
}
|
||||
|
||||
export function GetCoverFromFile(metadata: IAudioMetadata): string {
|
||||
if (metadata.common?.picture && metadata.common.picture.length > 0) {
|
||||
return URL.createObjectURL(new Blob(
|
||||
[metadata.common.picture[0].data],
|
||||
{type: metadata.common.picture[0].format}
|
||||
));
|
||||
}
|
||||
return "";
|
||||
if (metadata.common?.picture && metadata.common.picture.length > 0) {
|
||||
return URL.createObjectURL(
|
||||
new Blob([metadata.common.picture[0].data], { type: metadata.common.picture[0].format }),
|
||||
);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export interface IMusicMetaBasic {
|
||||
title: string
|
||||
artist?: string
|
||||
title: string;
|
||||
artist?: string;
|
||||
}
|
||||
|
||||
export function GetMetaFromFile(filename: string, exist_title?: string, exist_artist?: string, separator = "-")
|
||||
: IMusicMetaBasic {
|
||||
const meta: IMusicMetaBasic = {title: exist_title ?? "", artist: exist_artist}
|
||||
export function GetMetaFromFile(
|
||||
filename: string,
|
||||
exist_title?: string,
|
||||
exist_artist?: string,
|
||||
separator = '-',
|
||||
): IMusicMetaBasic {
|
||||
const meta: IMusicMetaBasic = { title: exist_title ?? '', artist: exist_artist };
|
||||
|
||||
const items = filename.split(separator);
|
||||
if (items.length > 1) {
|
||||
if (!meta.artist) meta.artist = items[0].trim();
|
||||
if (!meta.title) meta.title = items[1].trim();
|
||||
} else if (items.length === 1) {
|
||||
if (!meta.title) meta.title = items[0].trim();
|
||||
const items = filename.split(separator);
|
||||
if (items.length > 1) {
|
||||
if (!meta.artist) meta.artist = items[0].trim();
|
||||
if (!meta.title) meta.title = items[1].trim();
|
||||
} else if (items.length === 1) {
|
||||
if (!meta.title) meta.title = items[0].trim();
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
export async function GetImageFromURL(
|
||||
src: string,
|
||||
): Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> {
|
||||
try {
|
||||
const resp = await fetch(src);
|
||||
const mime = resp.headers.get('Content-Type');
|
||||
if (mime?.startsWith('image/')) {
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const url = URL.createObjectURL(new Blob([buffer], { type: mime }));
|
||||
return { buffer, url, mime };
|
||||
}
|
||||
return meta
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GetImageFromURL(src: string):
|
||||
Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> {
|
||||
try {
|
||||
const resp = await fetch(src);
|
||||
const mime = resp.headers.get("Content-Type");
|
||||
if (mime?.startsWith("image/")) {
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const url = URL.createObjectURL(new Blob([buffer], {type: mime}))
|
||||
return {buffer, url, mime}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface IMusicMeta {
|
||||
title: string
|
||||
artists?: string[]
|
||||
album?: string
|
||||
picture?: ArrayBuffer
|
||||
picture_desc?: string
|
||||
title: string;
|
||||
artists?: string[];
|
||||
album?: string;
|
||||
picture?: ArrayBuffer;
|
||||
picture_desc?: string;
|
||||
}
|
||||
|
||||
export function WriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
|
||||
const writer = new ID3Writer(audioData);
|
||||
const writer = new ID3Writer(audioData);
|
||||
|
||||
// reserve original data
|
||||
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || []
|
||||
frames.forEach(frame => {
|
||||
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
|
||||
try {
|
||||
writer.setFrame(frame.id, frame.value)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const old = original.common
|
||||
writer.setFrame('TPE1', old?.artists || info.artists || [])
|
||||
.setFrame('TIT2', old?.title || info.title)
|
||||
.setFrame('TALB', old?.album || info.album || "");
|
||||
if (info.picture) {
|
||||
writer.setFrame('APIC', {
|
||||
type: 3,
|
||||
data: info.picture,
|
||||
description: info.picture_desc || "Cover",
|
||||
})
|
||||
// reserve original data
|
||||
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
|
||||
frames.forEach((frame) => {
|
||||
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
|
||||
try {
|
||||
writer.setFrame(frame.id, frame.value);
|
||||
} catch (e) {}
|
||||
}
|
||||
return writer.addTag();
|
||||
});
|
||||
|
||||
const old = original.common;
|
||||
writer
|
||||
.setFrame('TPE1', old?.artists || info.artists || [])
|
||||
.setFrame('TIT2', old?.title || info.title)
|
||||
.setFrame('TALB', old?.album || info.album || '');
|
||||
if (info.picture) {
|
||||
writer.setFrame('APIC', {
|
||||
type: 3,
|
||||
data: info.picture,
|
||||
description: info.picture_desc || 'Cover',
|
||||
});
|
||||
}
|
||||
return writer.addTag();
|
||||
}
|
||||
|
||||
export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
|
||||
const writer = new MetaFlac(audioData)
|
||||
const old = original.common
|
||||
if (!old.title && !old.album && old.artists) {
|
||||
writer.setTag("TITLE=" + info.title)
|
||||
writer.setTag("ALBUM=" + info.album)
|
||||
if (info.artists) {
|
||||
writer.removeTag("ARTIST")
|
||||
info.artists.forEach(artist => writer.setTag("ARTIST=" + artist))
|
||||
}
|
||||
const writer = new MetaFlac(audioData);
|
||||
const old = original.common;
|
||||
if (!old.title && !old.album && old.artists) {
|
||||
writer.setTag('TITLE=' + info.title);
|
||||
writer.setTag('ALBUM=' + info.album);
|
||||
if (info.artists) {
|
||||
writer.removeTag('ARTIST');
|
||||
info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist));
|
||||
}
|
||||
}
|
||||
|
||||
if (info.picture) {
|
||||
writer.importPictureFromBuffer(Buffer.from(info.picture))
|
||||
}
|
||||
return writer.save()
|
||||
if (info.picture) {
|
||||
writer.importPictureFromBuffer(Buffer.from(info.picture));
|
||||
}
|
||||
return writer.save();
|
||||
}
|
||||
|
||||
export function SplitFilename(n: string): { name: string; ext: string } {
|
||||
const pos = n.lastIndexOf(".")
|
||||
return {
|
||||
ext: n.substring(pos + 1).toLowerCase(),
|
||||
name: n.substring(0, pos)
|
||||
}
|
||||
const pos = n.lastIndexOf('.');
|
||||
return {
|
||||
ext: n.substring(pos + 1).toLowerCase(),
|
||||
name: n.substring(0, pos),
|
||||
};
|
||||
}
|
||||
|
@@ -1,66 +1,67 @@
|
||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
|
||||
import {DecryptResult} from "@/decrypt/entity";
|
||||
import {AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile} from "@/decrypt/utils";
|
||||
import { Decrypt as RawDecrypt } from '@/decrypt/raw';
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
import { AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile } from '@/decrypt/utils';
|
||||
|
||||
import {parseBlob as metaParseBlob} from "music-metadata-browser";
|
||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||
|
||||
const MagicHeader = [0x69, 0x66, 0x6D, 0x74]
|
||||
const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe]
|
||||
const MagicHeader = [0x69, 0x66, 0x6d, 0x74];
|
||||
const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe];
|
||||
const FileTypeMap: { [key: string]: string } = {
|
||||
" WAV": ".wav",
|
||||
"FLAC": ".flac",
|
||||
" MP3": ".mp3",
|
||||
" A4M": ".m4a",
|
||||
}
|
||||
' WAV': '.wav',
|
||||
FLAC: '.flac',
|
||||
' MP3': '.mp3',
|
||||
' A4M': '.m4a',
|
||||
};
|
||||
|
||||
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
||||
if (!BytesHasPrefix(oriData, MagicHeader) || !BytesHasPrefix(oriData.slice(8, 12), MagicHeader2)) {
|
||||
if (raw_ext === "xm") {
|
||||
throw Error("此xm文件已损坏")
|
||||
} else {
|
||||
return await RawDecrypt(file, raw_filename, raw_ext, true)
|
||||
}
|
||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
||||
if (!BytesHasPrefix(oriData, MagicHeader) || !BytesHasPrefix(oriData.slice(8, 12), MagicHeader2)) {
|
||||
if (raw_ext === 'xm') {
|
||||
throw Error('此xm文件已损坏');
|
||||
} else {
|
||||
return await RawDecrypt(file, raw_filename, raw_ext, true);
|
||||
}
|
||||
}
|
||||
|
||||
let typeText = (new TextDecoder()).decode(oriData.slice(4, 8))
|
||||
if (!FileTypeMap.hasOwnProperty(typeText)) {
|
||||
throw Error("未知的.xm文件类型")
|
||||
}
|
||||
let typeText = new TextDecoder().decode(oriData.slice(4, 8));
|
||||
if (!FileTypeMap.hasOwnProperty(typeText)) {
|
||||
throw Error('未知的.xm文件类型');
|
||||
}
|
||||
|
||||
let key = oriData[0xf]
|
||||
let dataOffset = oriData[0xc] | oriData[0xd] << 8 | oriData[0xe] << 16
|
||||
let audioData = oriData.slice(0x10);
|
||||
let lenAudioData = audioData.length;
|
||||
for (let cur = dataOffset; cur < lenAudioData; ++cur)
|
||||
audioData[cur] = (audioData[cur] - key) ^ 0xff;
|
||||
let key = oriData[0xf];
|
||||
let dataOffset = oriData[0xc] | (oriData[0xd] << 8) | (oriData[0xe] << 16);
|
||||
let audioData = oriData.slice(0x10);
|
||||
let lenAudioData = audioData.length;
|
||||
for (let cur = dataOffset; cur < lenAudioData; ++cur) audioData[cur] = (audioData[cur] - key) ^ 0xff;
|
||||
|
||||
const ext = FileTypeMap[typeText];
|
||||
const mime = AudioMimeType[ext];
|
||||
let musicBlob = new Blob([audioData], {type: mime});
|
||||
const ext = FileTypeMap[typeText];
|
||||
const mime = AudioMimeType[ext];
|
||||
let musicBlob = new Blob([audioData], { type: mime });
|
||||
|
||||
const musicMeta = await metaParseBlob(musicBlob);
|
||||
if (ext === "wav") {
|
||||
//todo:未知的编码方式
|
||||
console.info(musicMeta.common)
|
||||
musicMeta.common.album = "";
|
||||
musicMeta.common.artist = "";
|
||||
musicMeta.common.title = "";
|
||||
}
|
||||
const {title, artist} = GetMetaFromFile(raw_filename,
|
||||
musicMeta.common.title, musicMeta.common.artist,
|
||||
raw_filename.indexOf("_") === -1 ? "-" : "_")
|
||||
const musicMeta = await metaParseBlob(musicBlob);
|
||||
if (ext === 'wav') {
|
||||
//todo:未知的编码方式
|
||||
console.info(musicMeta.common);
|
||||
musicMeta.common.album = '';
|
||||
musicMeta.common.artist = '';
|
||||
musicMeta.common.title = '';
|
||||
}
|
||||
const { title, artist } = GetMetaFromFile(
|
||||
raw_filename,
|
||||
musicMeta.common.title,
|
||||
musicMeta.common.artist,
|
||||
raw_filename.indexOf('_') === -1 ? '-' : '_',
|
||||
);
|
||||
|
||||
return {
|
||||
title,
|
||||
artist,
|
||||
ext,
|
||||
mime,
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
rawExt: "xm"
|
||||
}
|
||||
return {
|
||||
title,
|
||||
artist,
|
||||
ext,
|
||||
mime,
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
file: URL.createObjectURL(musicBlob),
|
||||
blob: musicBlob,
|
||||
rawExt: 'xm',
|
||||
};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user