diff --git a/src/App.vue b/src/App.vue index b130530..a0b2caa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,85 +1,87 @@ diff --git a/src/component/FileSelector.vue b/src/component/FileSelector.vue index 126a417..1ae82ac 100644 --- a/src/component/FileSelector.vue +++ b/src/component/FileSelector.vue @@ -1,99 +1,90 @@ - diff --git a/src/component/PreviewTable.vue b/src/component/PreviewTable.vue index 2385f5c..dbd9c2d 100644 --- a/src/component/PreviewTable.vue +++ b/src/component/PreviewTable.vue @@ -1,71 +1,62 @@ - + diff --git a/src/decrypt/common.ts b/src/decrypt/common.ts index 874016f..ec7c935 100644 --- a/src/decrypt/common.ts +++ b/src/decrypt/common.ts @@ -1,81 +1,79 @@ -import {Decrypt as NcmDecrypt} from "@/decrypt/ncm"; -import {Decrypt as NcmCacheDecrypt} from "@/decrypt/ncmcache"; -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 NcmDecrypt } from '@/decrypt/ncm'; +import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache'; +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 { - const raw = SplitFilename(file.name) - let rt_data: DecryptResult; - switch (raw.ext) { - case "ncm":// Netease Mp3/Flac - rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext); - break; - case "uc":// Netease Cache - rt_data = await NcmCacheDecrypt(file.raw, raw.name, raw.ext); - break; - 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 'ncm': // Netease Mp3/Flac + rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext); + break; + case 'uc': // Netease Cache + rt_data = await NcmCacheDecrypt(file.raw, raw.name, raw.ext); + break; + 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; } - diff --git a/src/decrypt/entity.ts b/src/decrypt/entity.ts index 3736f57..f2f6831 100644 --- a/src/decrypt/entity.ts +++ b/src/decrypt/entity.ts @@ -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; } diff --git a/src/decrypt/kgm.ts b/src/decrypt/kgm.ts index e6987ef..06f4373 100644 --- a/src/decrypt/kgm.ts +++ b/src/decrypt/kgm.ts @@ -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 { + 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("文件过大,请使用 CLI版本 进行解锁"); + } - let audioData = oriData.slice(headerLen) - let dataLen = audioData.length - if (audioData.byteLength > 1 << 26) { - throw Error("文件过大,请使用 CLI版本 进行解锁") - } + 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 { - 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, +]; diff --git a/src/decrypt/kwm.ts b/src/decrypt/kwm.ts index 4a6484a..0566fc9 100644 --- a/src/decrypt/kwm.ts +++ b/src/decrypt/kwm.ts @@ -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 { - 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; } diff --git a/src/decrypt/ncm.ts b/src/decrypt/ncm.ts index bb934b6..c2f9851 100644 --- a/src/decrypt/ncm.ts +++ b/src/decrypt/ncm.ts @@ -1,244 +1,237 @@ import { - AudioMimeType, - BytesHasPrefix, - GetArrayBuffer, - GetImageFromURL, - GetMetaFromFile, - IMusicMeta, - SniffAudioExt, - WriteMetaToFlac, - WriteMetaToMp3 -} from "@/decrypt/utils"; -import {parseBlob as metaParseBlob} from "music-metadata-browser"; + AudioMimeType, + BytesHasPrefix, + GetArrayBuffer, + GetImageFromURL, + GetMetaFromFile, + IMusicMeta, + SniffAudioExt, + WriteMetaToFlac, + WriteMetaToMp3, +} from '@/decrypt/utils'; +import { parseBlob as metaParseBlob } from 'music-metadata-browser'; import jimp from 'jimp'; -import AES from "crypto-js/aes"; -import PKCS7 from "crypto-js/pad-pkcs7"; -import ModeECB from "crypto-js/mode-ecb"; -import WordArray from "crypto-js/lib-typedarrays"; -import Base64 from "crypto-js/enc-base64"; -import EncUTF8 from "crypto-js/enc-utf8"; -import EncHex from "crypto-js/enc-hex"; +import AES from 'crypto-js/aes'; +import PKCS7 from 'crypto-js/pad-pkcs7'; +import ModeECB from 'crypto-js/mode-ecb'; +import WordArray from 'crypto-js/lib-typedarrays'; +import Base64 from 'crypto-js/enc-base64'; +import EncUTF8 from 'crypto-js/enc-utf8'; +import EncHex from 'crypto-js/enc-hex'; -import {DecryptResult} from "@/decrypt/entity"; - -const CORE_KEY = EncHex.parse("687a4852416d736f356b496e62617857"); -const META_KEY = EncHex.parse("2331346C6A6B5F215C5D2630553C2728"); -const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D]; +import { DecryptResult } from '@/decrypt/entity'; +const CORE_KEY = EncHex.parse('687a4852416d736f356b496e62617857'); +const META_KEY = EncHex.parse('2331346C6A6B5F215C5D2630553C2728'); +const MagicHeader = [0x43, 0x54, 0x45, 0x4e, 0x46, 0x44, 0x41, 0x4d]; export async function Decrypt(file: File, raw_filename: string, _: string): Promise { - return (new NcmDecrypt(await GetArrayBuffer(file), raw_filename)).decrypt() + return new NcmDecrypt(await GetArrayBuffer(file), raw_filename).decrypt(); } - interface NcmMusicMeta { - //musicId: number - musicName?: string - artist?: Array[] - format?: string - album?: string - albumPic?: string + //musicId: number + musicName?: string; + artist?: Array[]; + format?: string; + album?: string; + albumPic?: string; } interface NcmDjMeta { - mainMusic: NcmMusicMeta + mainMusic: NcmMusicMeta; } - class NcmDecrypt { - raw: ArrayBuffer - view: DataView - offset: number = 0 - filename: string - format: string = "" - mime: string = "" - audio?: Uint8Array - blob?: Blob - oriMeta?: NcmMusicMeta - newMeta?: IMusicMeta - image?: { mime: string, buffer: ArrayBuffer, url: string } + raw: ArrayBuffer; + view: DataView; + offset: number = 0; + filename: string; + format: string = ''; + mime: string = ''; + audio?: Uint8Array; + blob?: Blob; + oriMeta?: NcmMusicMeta; + newMeta?: IMusicMeta; + image?: { mime: string; buffer: ArrayBuffer; url: string }; - constructor(buf: ArrayBuffer, filename: string) { - const prefix = new Uint8Array(buf, 0, 8) - if (!BytesHasPrefix(prefix, MagicHeader)) throw Error("此ncm文件已损坏") - this.offset = 10 - this.raw = buf - this.view = new DataView(buf) - this.filename = filename + constructor(buf: ArrayBuffer, filename: string) { + const prefix = new Uint8Array(buf, 0, 8); + if (!BytesHasPrefix(prefix, MagicHeader)) throw Error('此ncm文件已损坏'); + this.offset = 10; + this.raw = buf; + this.view = new DataView(buf); + this.filename = filename; + } + + _getKeyData(): Uint8Array { + const keyLen = this.view.getUint32(this.offset, true); + this.offset += 4; + const cipherText = new Uint8Array(this.raw, this.offset, keyLen).map((uint8) => uint8 ^ 0x64); + this.offset += keyLen; + + const plainText = AES.decrypt( + // @ts-ignore + { ciphertext: WordArray.create(cipherText) }, + CORE_KEY, + { mode: ModeECB, padding: PKCS7 }, + ); + + const result = new Uint8Array(plainText.sigBytes); + + const words = plainText.words; + const sigBytes = plainText.sigBytes; + for (let i = 0; i < sigBytes; i++) { + result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; } - _getKeyData(): Uint8Array { - const keyLen = this.view.getUint32(this.offset, true); - this.offset += 4; - const cipherText = new Uint8Array(this.raw, this.offset, keyLen) - .map(uint8 => uint8 ^ 0x64); - this.offset += keyLen; + return result.slice(17); + } - const plainText = AES.decrypt( - // @ts-ignore - {ciphertext: WordArray.create(cipherText)}, - CORE_KEY, - {mode: ModeECB, padding: PKCS7} - ); + _getKeyBox(): Uint8Array { + const keyData = this._getKeyData(); + const box = new Uint8Array(Array(256).keys()); - const result = new Uint8Array(plainText.sigBytes); + const keyDataLen = keyData.length; - const words = plainText.words; - const sigBytes = plainText.sigBytes; - for (let i = 0; i < sigBytes; i++) { - result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; - } + let j = 0; - return result.slice(17) + for (let i = 0; i < 256; i++) { + j = (box[i] + j + keyData[i % keyDataLen]) & 0xff; + [box[i], box[j]] = [box[j], box[i]]; } - _getKeyBox(): Uint8Array { - const keyData = this._getKeyData() - const box = new Uint8Array(Array(256).keys()); + return box.map((_, i, arr) => { + i = (i + 1) & 0xff; + const si = arr[i]; + const sj = arr[(i + si) & 0xff]; + return arr[(si + sj) & 0xff]; + }); + } - const keyDataLen = keyData.length; + _getMetaData(): NcmMusicMeta { + const metaDataLen = this.view.getUint32(this.offset, true); + this.offset += 4; + if (metaDataLen === 0) return {}; - let j = 0; + const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen).map((data) => data ^ 0x63); + this.offset += metaDataLen; - for (let i = 0; i < 256; i++) { - j = (box[i] + j + keyData[i % keyDataLen]) & 0xff; - [box[i], box[j]] = [box[j], box[i]]; - } + WordArray.create(); + const plainText = AES.decrypt( + // @ts-ignore + { + ciphertext: Base64.parse( + // @ts-ignore + WordArray.create(cipherText.slice(22)).toString(EncUTF8), + ), + }, + META_KEY, + { mode: ModeECB, padding: PKCS7 }, + ).toString(EncUTF8); - return box.map((_, i, arr) => { - i = (i + 1) & 0xff; - const si = arr[i]; - const sj = arr[(i + si) & 0xff]; - return arr[(si + sj) & 0xff]; - }); + const labelIndex = plainText.indexOf(':'); + let result: NcmMusicMeta; + if (plainText.slice(0, labelIndex) === 'dj') { + const tmp: NcmDjMeta = JSON.parse(plainText.slice(labelIndex + 1)); + result = tmp.mainMusic; + } else { + result = JSON.parse(plainText.slice(labelIndex + 1)); + } + if (!!result.albumPic) { + result.albumPic = result.albumPic.replace('http://', 'https://') + '?param=500y500'; + } + return result; + } + + _getAudio(keyBox: Uint8Array): Uint8Array { + this.offset += this.view.getUint32(this.offset + 5, true) + 13; + const audioData = new Uint8Array(this.raw, this.offset); + let lenAudioData = audioData.length; + for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff]; + return audioData; + } + + async _buildMeta() { + if (!this.oriMeta) throw Error('invalid sequence'); + + const info = GetMetaFromFile(this.filename, this.oriMeta.musicName); + + // build artists + let artists: string[] = []; + if (!!this.oriMeta.artist) { + this.oriMeta.artist.forEach((arr) => artists.push(arr[0])); } - _getMetaData(): NcmMusicMeta { - const metaDataLen = this.view.getUint32(this.offset, true); - this.offset += 4; - if (metaDataLen === 0) return {}; - - const cipherText = new Uint8Array(this.raw, this.offset, metaDataLen) - .map(data => data ^ 0x63); - this.offset += metaDataLen; - - WordArray.create() - const plainText = AES.decrypt( - // @ts-ignore - { - ciphertext: Base64.parse( - // @ts-ignore - WordArray.create(cipherText.slice(22)).toString(EncUTF8) - ) - }, - META_KEY, - {mode: ModeECB, padding: PKCS7} - ).toString(EncUTF8); - - const labelIndex = plainText.indexOf(":"); - let result: NcmMusicMeta; - if (plainText.slice(0, labelIndex) === "dj") { - const tmp: NcmDjMeta = JSON.parse(plainText.slice(labelIndex + 1)); - result = tmp.mainMusic; - } else { - result = JSON.parse(plainText.slice(labelIndex + 1)); - } - if (!!result.albumPic) { - result.albumPic = result.albumPic.replace("http://", "https://") + "?param=500y500" - } - return result + if (artists.length === 0 && !!info.artist) { + artists = info.artist + .split(',') + .map((val) => val.trim()) + .filter((val) => val != ''); } - _getAudio(keyBox: Uint8Array): Uint8Array { - this.offset += this.view.getUint32(this.offset + 5, true) + 13 - const audioData = new Uint8Array(this.raw, this.offset) - let lenAudioData = audioData.length - for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff] - return audioData + if (this.oriMeta.albumPic) + try { + this.image = await GetImageFromURL(this.oriMeta.albumPic); + while (this.image && this.image.buffer.byteLength >= 1 << 24) { + let img = await jimp.read(Buffer.from(this.image.buffer)); + await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO); + this.image.buffer = await img.getBufferAsync('image/jpeg'); + } + } catch (e) { + console.log('get cover image failed', e); + } + + this.newMeta = { title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer }; + } + + async _writeMeta() { + if (!this.audio || !this.newMeta) throw Error('invalid sequence'); + + if (!this.blob) this.blob = new Blob([this.audio], { type: this.mime }); + const ori = await metaParseBlob(this.blob); + + let shouldWrite = !ori.common.album && !ori.common.artists && !ori.common.title; + if (shouldWrite || this.newMeta.picture) { + if (this.format === 'mp3') { + this.audio = WriteMetaToMp3(Buffer.from(this.audio), this.newMeta, ori); + } else if (this.format === 'flac') { + this.audio = WriteMetaToFlac(Buffer.from(this.audio), this.newMeta, ori); + } else { + console.info(`writing meta for ${this.format} is not being supported for now`); + return; + } + this.blob = new Blob([this.audio], { type: this.mime }); } + } - async _buildMeta() { - if (!this.oriMeta) throw Error("invalid sequence") + gatherResult(): DecryptResult { + if (!this.newMeta || !this.blob) throw Error('bad sequence'); + return { + title: this.newMeta.title, + artist: this.newMeta.artists?.join('; '), + ext: this.format, + album: this.newMeta.album, + picture: this.image?.url, + file: URL.createObjectURL(this.blob), + blob: this.blob, + mime: this.mime, + }; + } - const info = GetMetaFromFile(this.filename, this.oriMeta.musicName) - - // build artists - let artists: string[] = []; - if (!!this.oriMeta.artist) { - this.oriMeta.artist.forEach(arr => artists.push(arr[0])); - } - - if (artists.length === 0 && !!info.artist) { - artists = info.artist.split(',') - .map(val => val.trim()).filter(val => val != ""); - } - - if (this.oriMeta.albumPic) try { - this.image = await GetImageFromURL(this.oriMeta.albumPic) - while (this.image && this.image.buffer.byteLength >= 1 << 24) { - let img = await jimp.read(Buffer.from(this.image.buffer)) - await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO) - this.image.buffer = await img.getBufferAsync("image/jpeg") - } - } catch (e) { - console.log("get cover image failed", e) - } - - - this.newMeta = {title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer} + async decrypt() { + const keyBox = this._getKeyBox(); + this.oriMeta = this._getMetaData(); + this.audio = this._getAudio(keyBox); + this.format = this.oriMeta.format || SniffAudioExt(this.audio); + this.mime = AudioMimeType[this.format]; + await this._buildMeta(); + try { + await this._writeMeta(); + } catch (e) { + console.warn('write meta data failed', e); } - - async _writeMeta() { - if (!this.audio || !this.newMeta) throw Error("invalid sequence") - - if (!this.blob) this.blob = new Blob([this.audio], {type: this.mime}) - const ori = await metaParseBlob(this.blob); - - let shouldWrite = !ori.common.album && !ori.common.artists && !ori.common.title - if (shouldWrite || this.newMeta.picture) { - if (this.format === "mp3") { - this.audio = WriteMetaToMp3(Buffer.from(this.audio), this.newMeta, ori) - } else if (this.format === "flac") { - this.audio = WriteMetaToFlac(Buffer.from(this.audio), this.newMeta, ori) - } else { - console.info(`writing meta for ${this.format} is not being supported for now`) - return - } - this.blob = new Blob([this.audio], {type: this.mime}) - } - } - - gatherResult(): DecryptResult { - if (!this.newMeta || !this.blob) throw Error("bad sequence") - return { - title: this.newMeta.title, - artist: this.newMeta.artists?.join("; "), - ext: this.format, - album: this.newMeta.album, - picture: this.image?.url, - file: URL.createObjectURL(this.blob), - blob: this.blob, - mime: this.mime - } - } - - async decrypt() { - const keyBox = this._getKeyBox() - this.oriMeta = this._getMetaData() - this.audio = this._getAudio(keyBox) - this.format = this.oriMeta.format || SniffAudioExt(this.audio) - this.mime = AudioMimeType[this.format] - await this._buildMeta() - try { - await this._writeMeta() - } catch (e) { - console.warn("write meta data failed", e) - } - return this.gatherResult() - } - - + return this.gatherResult(); + } } - - diff --git a/src/decrypt/ncmcache.ts b/src/decrypt/ncmcache.ts index 0c031a3..9b72d0c 100644 --- a/src/decrypt/ncmcache.ts +++ b/src/decrypt/ncmcache.ts @@ -1,29 +1,28 @@ -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) - : Promise { - const buffer = new Uint8Array(await GetArrayBuffer(file)); - let length = buffer.length - for (let i = 0; i < length; i++) { - buffer[i] ^= 163 - } - const 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): Promise { + const buffer = new Uint8Array(await GetArrayBuffer(file)); + let length = buffer.length; + for (let i = 0; i < length; i++) { + buffer[i] ^= 163; + } + const 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], + }; } diff --git a/src/decrypt/qmc.test.ts b/src/decrypt/qmc.test.ts index 5bb2af9..9fee8ee 100644 --- a/src/decrypt/qmc.test.ts +++ b/src/decrypt/qmc.test.ts @@ -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(); } -}) - - - +}); diff --git a/src/decrypt/qmc.ts b/src/decrypt/qmc.ts index 2b5c6d7..c026c35 100644 --- a/src/decrypt/qmc.ts +++ b/src/decrypt/qmc.ts @@ -1,4 +1,4 @@ -import {QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher} from "./qmc_cipher"; +import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher'; import { AudioMimeType, GetArrayBuffer, @@ -7,56 +7,55 @@ 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 { if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`; const handler = HandlerMap[raw_ext]; - let {version} = handler; + let { version } = handler; const fileBuffer = await GetArrayBuffer(file); 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 { - 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); } } - - } diff --git a/src/decrypt/qmc_cipher.test.ts b/src/decrypt/qmc_cipher.test.ts index a54d0f8..4c574f7 100644 --- a/src/decrypt/qmc_cipher.test.ts +++ b/src/decrypt/qmc_cipher.test.ts @@ -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)); } -}) +}); diff --git a/src/decrypt/qmc_cipher.ts b/src/decrypt/qmc_cipher.ts index a6bc53e..14c0a50 100644 --- a/src/decrypt/qmc_cipher.ts +++ b/src/decrypt/qmc_cipher.ts @@ -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; } } diff --git a/src/decrypt/qmc_key.test.ts b/src/decrypt/qmc_key.test.ts index 74e37de..da392cc 100644 --- a/src/decrypt/qmc_key.test.ts +++ b/src/decrypt/qmc_key.test.ts @@ -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); } -}) +}); diff --git a/src/decrypt/qmc_key.ts b/src/decrypt/qmc_key.ts index 5914b34..e3385f7 100644 --- a/src/decrypt/qmc_key.ts +++ b/src/decrypt/qmc_key.ts @@ -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; } - diff --git a/src/decrypt/qmc_wasm.ts b/src/decrypt/qmc_wasm.ts index 7a1fd49..22e2c31 100644 --- a/src/decrypt/qmc_wasm.ts +++ b/src/decrypt/qmc_wasm.ts @@ -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)); diff --git a/src/decrypt/qmccache.ts b/src/decrypt/qmccache.ts index 3c25d58..dceb7cc 100644 --- a/src/decrypt/qmccache.ts +++ b/src/decrypt/qmccache.ts @@ -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 { - 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 { + 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], + }; } diff --git a/src/decrypt/raw.ts b/src/decrypt/raw.ts index 02209ca..083013f 100644 --- a/src/decrypt/raw.ts +++ b/src/decrypt/raw.ts @@ -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 { - 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 { + 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], + }; } diff --git a/src/decrypt/tm.ts b/src/decrypt/tm.ts index f1fd283..7aa717d 100644 --- a/src/decrypt/tm.ts +++ b/src/decrypt/tm.ts @@ -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 { - 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); } diff --git a/src/decrypt/utils.ts b/src/decrypt/utils.ts index 04b2125..45a896c 100644 --- a/src/decrypt/utils.ts +++ b/src/decrypt/utils.ts @@ -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 { - 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), + }; } diff --git a/src/decrypt/xm.ts b/src/decrypt/xm.ts index 3f7e90c..aba87d8 100644 --- a/src/decrypt/xm.ts +++ b/src/decrypt/xm.ts @@ -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 { - 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', + }; } - diff --git a/src/extension/popup.js b/src/extension/popup.js index 4d138c0..4bd1c71 100644 --- a/src/extension/popup.js +++ b/src/extension/popup.js @@ -1,5 +1,2 @@ -const bs = chrome || browser -bs.tabs.create({ - url: bs.runtime.getURL('./index.html') -}, tab => console.log(tab)) - +const bs = chrome || browser; +bs.tabs.create({ url: bs.runtime.getURL('./index.html') }, (tab) => console.log(tab)); diff --git a/src/main.ts b/src/main.ts index eafd516..97845b2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,25 +1,25 @@ -import Vue from 'vue' -import App from '@/App.vue' -import '@/registerServiceWorker' +import Vue from 'vue'; +import App from '@/App.vue'; +import '@/registerServiceWorker'; import { - Button, - Checkbox, - Col, - Container, - Footer, - Icon, - Image, - Link, - Main, - Notification, - Progress, - Radio, - Row, - Table, - TableColumn, - Tooltip, - Upload, - MessageBox + Button, + Checkbox, + Col, + Container, + Footer, + Icon, + Image, + Link, + Main, + Notification, + Progress, + Radio, + Row, + Table, + TableColumn, + Tooltip, + Upload, + MessageBox, } from 'element-ui'; import 'element-ui/lib/theme-chalk/base.css'; @@ -44,5 +44,5 @@ Vue.prototype.$confirm = MessageBox.confirm; Vue.config.productionTip = false; new Vue({ - render: h => h(App), + render: (h) => h(App), }).$mount('#app'); diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js index a2c6497..702b4f8 100644 --- a/src/registerServiceWorker.js +++ b/src/registerServiceWorker.js @@ -1,31 +1,30 @@ /* eslint-disable no-console */ -import {register} from 'register-service-worker' +import { register } from 'register-service-worker'; -if (process.env.NODE_ENV === 'production' && window.location.protocol === "https:") { - - register(`${process.env.BASE_URL}service-worker.js`, { - ready() { - console.log('App is being served from cache by a service worker.') - }, - registered() { - console.log('Service worker has been registered.') - }, - cached() { - console.log('Content has been cached for offline use.') - }, - updatefound() { - console.log('New content is downloading.') - }, - updated() { - console.log('New content is available.'); - window.location.reload(); - }, - offline() { - console.log('No internet connection found. App is running in offline mode.') - }, - error(error) { - console.error('Error during service worker registration:', error) - } - }) +if (process.env.NODE_ENV === 'production' && window.location.protocol === 'https:') { + register(`${process.env.BASE_URL}service-worker.js`, { + ready() { + console.log('App is being served from cache by a service worker.'); + }, + registered() { + console.log('Service worker has been registered.'); + }, + cached() { + console.log('Content has been cached for offline use.'); + }, + updatefound() { + console.log('New content is downloading.'); + }, + updated() { + console.log('New content is available.'); + window.location.reload(); + }, + offline() { + console.log('No internet connection found. App is running in offline mode.'); + }, + error(error) { + console.error('Error during service worker registration:', error); + }, + }); } diff --git a/src/shims-browser-id3-writer.d.ts b/src/shims-browser-id3-writer.d.ts index 48113af..1f99de8 100644 --- a/src/shims-browser-id3-writer.d.ts +++ b/src/shims-browser-id3-writer.d.ts @@ -1,25 +1,23 @@ -declare module "browser-id3-writer" { - export default class ID3Writer { - constructor(buffer: Buffer | ArrayBuffer) +declare module 'browser-id3-writer' { + export default class ID3Writer { + constructor(buffer: Buffer | ArrayBuffer); - setFrame(name: string, value: string | object | string[]) + setFrame(name: string, value: string | object | string[]); - addTag(): Uint8Array - } + addTag(): Uint8Array; + } } -declare module "metaflac-js" { - export default class Metaflac { - constructor(buffer: Buffer) +declare module 'metaflac-js' { + export default class Metaflac { + constructor(buffer: Buffer); - setTag(field: string) + setTag(field: string); - removeTag(name: string) + removeTag(name: string); - importPictureFromBuffer(picture: Buffer) + importPictureFromBuffer(picture: Buffer); - save(): Buffer - } + save(): Buffer; + } } - - diff --git a/src/shims-fs.d.ts b/src/shims-fs.d.ts index c7ae721..da1f7f9 100644 --- a/src/shims-fs.d.ts +++ b/src/shims-fs.d.ts @@ -1,58 +1,54 @@ export interface FileSystemGetFileOptions { - create?: boolean + create?: boolean; } interface FileSystemCreateWritableOptions { - keepExistingData?: boolean + keepExistingData?: boolean; } interface FileSystemRemoveOptions { - recursive?: boolean + recursive?: boolean; } interface FileSystemFileHandle { - getFile(): Promise; + getFile(): Promise; - createWritable(options?: FileSystemCreateWritableOptions): Promise + createWritable(options?: FileSystemCreateWritableOptions): Promise; } enum WriteCommandType { - write = "write", - seek = "seek", - truncate = "truncate", + write = 'write', + seek = 'seek', + truncate = 'truncate', } interface WriteParams { - type: WriteCommandType - size?: number - position?: number - data: BufferSource | Blob | string + type: WriteCommandType; + size?: number; + position?: number; + data: BufferSource | Blob | string; } -type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams +type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams; interface FileSystemWritableFileStream extends WritableStream { - write(data: FileSystemWriteChunkType): Promise + write(data: FileSystemWriteChunkType): Promise; - seek(position: number): Promise + seek(position: number): Promise; - truncate(size: number): Promise + truncate(size: number): Promise; - close(): Promise // should be implemented in WritableStream + close(): Promise; // should be implemented in WritableStream } - export declare interface FileSystemDirectoryHandle { - getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise - - removeEntry(name: string, options?: FileSystemRemoveOptions): Promise + getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise; + removeEntry(name: string, options?: FileSystemRemoveOptions): Promise; } declare global { - interface Window { - - showDirectoryPicker?(): Promise - } + interface Window { + showDirectoryPicker?(): Promise; + } } - diff --git a/src/shims-tsx.d.ts b/src/shims-tsx.d.ts index cc8d7a1..19b2740 100644 --- a/src/shims-tsx.d.ts +++ b/src/shims-tsx.d.ts @@ -1,17 +1,15 @@ -import Vue, {VNode} from 'vue' +import Vue, { VNode } from 'vue'; declare global { namespace JSX { // tslint:disable no-empty-interface - interface Element extends VNode { - } + interface Element extends VNode {} // tslint:disable no-empty-interface - interface ElementClass extends Vue { - } + interface ElementClass extends Vue {} interface IntrinsicElements { - [elem: string]: any + [elem: string]: any; } } } diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts index d9f24fa..8f6f410 100644 --- a/src/shims-vue.d.ts +++ b/src/shims-vue.d.ts @@ -1,4 +1,4 @@ declare module '*.vue' { - import Vue from 'vue' - export default Vue + import Vue from 'vue'; + export default Vue; } diff --git a/src/utils/api.ts b/src/utils/api.ts index 0dfc6d3..9bc0dcf 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,56 +1,73 @@ -import {fromByteArray as Base64Encode} from "base64-js"; +import { fromByteArray as Base64Encode } from 'base64-js'; -export const IXAREA_API_ENDPOINT = "https://um-api.ixarea.com" +export const IXAREA_API_ENDPOINT = 'https://um-api.ixarea.com'; export interface UpdateInfo { - Found: boolean - HttpsFound: boolean - Version: string - URL: string - Detail: string + Found: boolean; + HttpsFound: boolean; + Version: string; + URL: string; + Detail: string; } export async function checkUpdate(version: string): Promise { - const resp = await fetch(IXAREA_API_ENDPOINT + "/music/app-version", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({"Version": version}) - }); - return await resp.json(); + const resp = await fetch(IXAREA_API_ENDPOINT + '/music/app-version', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Version: version }), + }); + return await resp.json(); } -export function reportKeyUsage(keyData: Uint8Array, maskData: number[], filename: string, format: string, title: string, artist?: string, album?: string) { - return fetch(IXAREA_API_ENDPOINT + "/qmcmask/usage", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({ - Mask: Base64Encode(new Uint8Array(maskData)), Key: Base64Encode(keyData), - Artist: artist, Title: title, Album: album, Filename: filename, Format: format - }), - }) +export function reportKeyUsage( + keyData: Uint8Array, + maskData: number[], + filename: string, + format: string, + title: string, + artist?: string, + album?: string, +) { + return fetch(IXAREA_API_ENDPOINT + '/qmcmask/usage', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + Mask: Base64Encode(new Uint8Array(maskData)), + Key: Base64Encode(keyData), + Artist: artist, + Title: title, + Album: album, + Filename: filename, + Format: format, + }), + }); } interface KeyInfo { - Matrix44: string + Matrix44: string; } export async function queryKeyInfo(keyData: Uint8Array, filename: string, format: string): Promise { - const resp = await fetch(IXAREA_API_ENDPOINT + "/qmcmask/query", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44}), - }); - return await resp.json(); + const resp = await fetch(IXAREA_API_ENDPOINT + '/qmcmask/query', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Format: format, Key: Base64Encode(keyData), Filename: filename, Type: 44 }), + }); + return await resp.json(); } export interface CoverInfo { - Id: string - Type: number + Id: string; + Type: number; } export async function queryAlbumCover(title: string, artist?: string, album?: string): Promise { - const endpoint = IXAREA_API_ENDPOINT + "/music/qq-cover" - const params = new URLSearchParams([["Title", title], ["Artist", artist ?? ""], ["Album", album ?? ""]]) - const resp = await fetch(`${endpoint}?${params.toString()}`) - return await resp.json() + const endpoint = IXAREA_API_ENDPOINT + '/music/qq-cover'; + const params = new URLSearchParams([ + ['Title', title], + ['Artist', artist ?? ''], + ['Album', album ?? ''], + ]); + const resp = await fetch(`${endpoint}?${params.toString()}`); + return await resp.json(); } diff --git a/src/utils/tea.test.ts b/src/utils/tea.test.ts index a3f9273..447fc55 100644 --- a/src/utils/tea.test.ts +++ b/src/utils/tea.test.ts @@ -4,74 +4,67 @@ // Use of this source code is governed by a BSD-style // license that can be found in https://go.dev/LICENSE. -import {TeaCipher} from "@/utils/tea"; +import { TeaCipher } from '@/utils/tea'; +test('key size', () => { + // prettier-ignore + const testKey = new Uint8Array([ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + 0x00, + ]) + expect(() => new TeaCipher(testKey.slice(0, 16))).not.toThrow(); -test("key size", () => { - const testKey = new Uint8Array([ - 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, - 0x00 - ]) - expect(() => new TeaCipher(testKey.slice(0, 16))) - .not.toThrow() - - expect(() => new TeaCipher(testKey)) - .toThrow() - - expect(() => new TeaCipher(testKey.slice(0, 15))) - .toThrow() - -}) + expect(() => new TeaCipher(testKey)).toThrow(); + expect(() => new TeaCipher(testKey.slice(0, 15))).toThrow(); +}); +// prettier-ignore const teaTests = [ - // These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec - { - rounds: TeaCipher.numRounds, - key: new Uint8Array([ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]), - }, - { - rounds: TeaCipher.numRounds, - key: new Uint8Array([ - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), - plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), - cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]), - }, - { - rounds: 16, - key: new Uint8Array([ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]), - }, -] + // These were sourced from https://github.com/froydnj/ironclad/blob/master/testing/test-vectors/tea.testvec + { + rounds: TeaCipher.numRounds, + key: new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]), + plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + cipherText: new Uint8Array([0x41, 0xea, 0x3a, 0x0a, 0x94, 0xba, 0xa9, 0x40]), + }, + { + rounds: TeaCipher.numRounds, + key: new Uint8Array([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]), + plainText: new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + cipherText: new Uint8Array([0x31, 0x9b, 0xbe, 0xfb, 0x01, 0x6a, 0xbd, 0xb2]), + }, + { + rounds: 16, + key: new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]), + plainText: new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + cipherText: new Uint8Array([0xed, 0x28, 0x5d, 0xa1, 0x45, 0x5b, 0x33, 0xc1]), + }, +]; -test("rounds", () => { - const tt = teaTests[0]; - expect(() => new TeaCipher(tt.key, tt.rounds - 1)) - .toThrow() -}) +test('rounds', () => { + const tt = teaTests[0]; + expect(() => new TeaCipher(tt.key, tt.rounds - 1)).toThrow(); +}); +test('encrypt & decrypt', () => { + for (const tt of teaTests) { + const c = new TeaCipher(tt.key, tt.rounds); -test("encrypt & decrypt", () => { - for (const tt of teaTests) { - const c = new TeaCipher(tt.key, tt.rounds) + const buf = new Uint8Array(8); + const bufView = new DataView(buf.buffer); - const buf = new Uint8Array(8) - const bufView = new DataView(buf.buffer) - - c.encrypt(bufView, new DataView(tt.plainText.buffer)) - expect(buf).toStrictEqual(tt.cipherText) - - c.decrypt(bufView, new DataView(tt.cipherText.buffer)) - expect(buf).toStrictEqual(tt.plainText) - } -}) + c.encrypt(bufView, new DataView(tt.plainText.buffer)); + expect(buf).toStrictEqual(tt.cipherText); + c.decrypt(bufView, new DataView(tt.cipherText.buffer)); + expect(buf).toStrictEqual(tt.plainText); + } +}); diff --git a/src/utils/tea.ts b/src/utils/tea.ts index 9b4f57a..0a93d6a 100644 --- a/src/utils/tea.ts +++ b/src/utils/tea.ts @@ -15,68 +15,66 @@ // where compatibility with legacy systems, not security, is the goal. export class TeaCipher { - // BlockSize is the size of a TEA block, in bytes. - static readonly BlockSize = 8; + // BlockSize is the size of a TEA block, in bytes. + static readonly BlockSize = 8; - // KeySize is the size of a TEA key, in bytes. - static readonly KeySize = 16; + // KeySize is the size of a TEA key, in bytes. + static readonly KeySize = 16; - // delta is the TEA key schedule constant. - static readonly delta = 0x9e3779b9; + // delta is the TEA key schedule constant. + static readonly delta = 0x9e3779b9; - // numRounds 64 is the standard number of rounds in TEA. - static readonly numRounds = 64; + // numRounds 64 is the standard number of rounds in TEA. + static readonly numRounds = 64; - k0: number - k1: number - k2: number - k3: number - rounds: number + k0: number; + k1: number; + k2: number; + k3: number; + rounds: number; - constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) { - if (key.length != 16) { - throw Error("incorrect key size") - } - if ((rounds & 1) != 0) { - throw Error("odd number of rounds specified") - } - - const k = new DataView(key.buffer) - this.k0 = k.getUint32(0, false) - this.k1 = k.getUint32(4, false) - this.k2 = k.getUint32(8, false) - this.k3 = k.getUint32(12, false) - this.rounds = rounds + constructor(key: Uint8Array, rounds: number = TeaCipher.numRounds) { + if (key.length != 16) { + throw Error('incorrect key size'); + } + if ((rounds & 1) != 0) { + throw Error('odd number of rounds specified'); } + const k = new DataView(key.buffer); + this.k0 = k.getUint32(0, false); + this.k1 = k.getUint32(4, false); + this.k2 = k.getUint32(8, false); + this.k3 = k.getUint32(12, false); + this.rounds = rounds; + } - encrypt(dst: DataView, src: DataView) { + encrypt(dst: DataView, src: DataView) { + let v0 = src.getUint32(0, false); + let v1 = src.getUint32(4, false); - let v0 = src.getUint32(0, false) - let v1 = src.getUint32(4, false) - - let sum = 0 - for (let i = 0; i < this.rounds / 2; i++) { - sum = sum + TeaCipher.delta - v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1) - v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3) - } - - dst.setUint32(0, v0, false) - dst.setUint32(4, v1, false) + let sum = 0; + for (let i = 0; i < this.rounds / 2; i++) { + sum = sum + TeaCipher.delta; + v0 += ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1); + v1 += ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3); } - decrypt(dst: DataView, src: DataView) { - let v0 = src.getUint32(0, false) - let v1 = src.getUint32(4, false) + dst.setUint32(0, v0, false); + dst.setUint32(4, v1, false); + } - let sum = TeaCipher.delta * this.rounds / 2 - for (let i = 0; i < this.rounds / 2; i++) { - v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3) - v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1) - sum -= TeaCipher.delta - } - dst.setUint32(0, v0, false) - dst.setUint32(4, v1, false) + decrypt(dst: DataView, src: DataView) { + let v0 = src.getUint32(0, false); + let v1 = src.getUint32(4, false); + + let sum = (TeaCipher.delta * this.rounds) / 2; + for (let i = 0; i < this.rounds / 2; i++) { + v1 -= ((v0 << 4) + this.k2) ^ (v0 + sum) ^ ((v0 >>> 5) + this.k3); + v0 -= ((v1 << 4) + this.k0) ^ (v1 + sum) ^ ((v1 >>> 5) + this.k1); + sum -= TeaCipher.delta; } + dst.setUint32(0, v0, false); + dst.setUint32(4, v1, false); + } } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 42b9686..2917592 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,79 +1,80 @@ -import {DecryptResult} from "@/decrypt/entity"; -import {FileSystemDirectoryHandle} from "@/shims-fs"; +import { DecryptResult } from '@/decrypt/entity'; +import { FileSystemDirectoryHandle } from '@/shims-fs'; export enum FilenamePolicy { - ArtistAndTitle, - TitleOnly, - TitleAndArtist, - SameAsOriginal, + ArtistAndTitle, + TitleOnly, + TitleAndArtist, + SameAsOriginal, } -export const FilenamePolicies: { key: FilenamePolicy, text: string }[] = [ - {key: FilenamePolicy.ArtistAndTitle, text: "歌手-歌曲名"}, - {key: FilenamePolicy.TitleOnly, text: "歌曲名"}, - {key: FilenamePolicy.TitleAndArtist, text: "歌曲名-歌手"}, - {key: FilenamePolicy.SameAsOriginal, text: "同源文件名"}, -] +export const FilenamePolicies: { key: FilenamePolicy; text: string }[] = [ + { key: FilenamePolicy.ArtistAndTitle, text: '歌手-歌曲名' }, + { key: FilenamePolicy.TitleOnly, text: '歌曲名' }, + { key: FilenamePolicy.TitleAndArtist, text: '歌曲名-歌手' }, + { key: FilenamePolicy.SameAsOriginal, text: '同源文件名' }, +]; export function GetDownloadFilename(data: DecryptResult, policy: FilenamePolicy): string { - switch (policy) { - case FilenamePolicy.TitleOnly: - return `${data.title}.${data.ext}`; - case FilenamePolicy.TitleAndArtist: - return `${data.title} - ${data.artist}.${data.ext}`; - case FilenamePolicy.SameAsOriginal: - return `${data.rawFilename}.${data.ext}`; - default: - case FilenamePolicy.ArtistAndTitle: - return `${data.artist} - ${data.title}.${data.ext}`; - } + switch (policy) { + case FilenamePolicy.TitleOnly: + return `${data.title}.${data.ext}`; + case FilenamePolicy.TitleAndArtist: + return `${data.title} - ${data.artist}.${data.ext}`; + case FilenamePolicy.SameAsOriginal: + return `${data.rawFilename}.${data.ext}`; + default: + case FilenamePolicy.ArtistAndTitle: + return `${data.artist} - ${data.title}.${data.ext}`; + } } export async function DirectlyWriteFile(data: DecryptResult, policy: FilenamePolicy, dir: FileSystemDirectoryHandle) { - let filename = GetDownloadFilename(data, policy) - // prevent filename exist - try { - await dir.getFileHandle(filename) - filename = `${new Date().getTime()} - ${filename}` - } catch (e) { - } - const file = await dir.getFileHandle(filename, {create: true}) - const w = await file.createWritable() - await w.write(data.blob) - await w.close() - + let filename = GetDownloadFilename(data, policy); + // prevent filename exist + try { + await dir.getFileHandle(filename); + filename = `${new Date().getTime()} - ${filename}`; + } catch (e) {} + const file = await dir.getFileHandle(filename, { create: true }); + const w = await file.createWritable(); + await w.write(data.blob); + await w.close(); } export function DownloadBlobMusic(data: DecryptResult, policy: FilenamePolicy) { - const a = document.createElement('a'); - a.href = data.file; - a.download = GetDownloadFilename(data, policy) - document.body.append(a); - a.click(); - a.remove(); + const a = document.createElement('a'); + a.href = data.file; + a.download = GetDownloadFilename(data, policy); + document.body.append(a); + a.click(); + a.remove(); } export function RemoveBlobMusic(data: DecryptResult) { - URL.revokeObjectURL(data.file); - if (data.picture?.startsWith("blob:")) { - URL.revokeObjectURL(data.picture); - } + URL.revokeObjectURL(data.file); + if (data.picture?.startsWith('blob:')) { + URL.revokeObjectURL(data.picture); + } } export class DecryptQueue { - private readonly pending: (() => Promise)[]; + private readonly pending: (() => Promise)[]; - constructor() { - this.pending = [] - } + constructor() { + this.pending = []; + } - queue(fn: () => Promise) { - this.pending.push(fn) - this.consume() - } + queue(fn: () => Promise) { + this.pending.push(fn); + this.consume(); + } - private consume() { - const fn = this.pending.shift() - if (fn) fn().then(() => this.consume).catch(console.error) - } + private consume() { + const fn = this.pending.shift(); + if (fn) + fn() + .then(() => this.consume) + .catch(console.error); + } } diff --git a/src/utils/worker.ts b/src/utils/worker.ts index c374ffc..f7a4a88 100644 --- a/src/utils/worker.ts +++ b/src/utils/worker.ts @@ -1,4 +1,4 @@ -import {expose} from "threads/worker"; -import {CommonDecrypt} from "@/decrypt/common"; +import { expose } from 'threads/worker'; +import { CommonDecrypt } from '@/decrypt/common'; -expose(CommonDecrypt) +expose(CommonDecrypt); diff --git a/src/view/Home.vue b/src/view/Home.vue index a337c59..d457bc5 100644 --- a/src/view/Home.vue +++ b/src/view/Home.vue @@ -1,157 +1,156 @@ -