diff --git a/.gitignore b/.gitignore index 7983440..9bf4141 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,10 @@ yarn-error.log* *.njsproj *.sln *.sw? + +/src/KgmWasm/build +/src/KgmWasm/build +/src/KgmWasm/*.js +/src/KgmWasm/*.wasm +/src/QmcWasm/*.js +/src/QmcWasm/*.wasm diff --git a/src/decrypt/index.ts b/src/decrypt/index.ts index 6fb14f3..3fae4a2 100644 --- a/src/decrypt/index.ts +++ b/src/decrypt/index.ts @@ -1,3 +1,4 @@ +import { Decrypt as Mg3dDecrypt } from '@/decrypt/mg3d'; import { Decrypt as NcmDecrypt } from '@/decrypt/ncm'; import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache'; import { Decrypt as XmDecrypt } from '@/decrypt/xm'; @@ -22,6 +23,9 @@ export async function Decrypt(file: FileInfo, config: Record): Prom const raw = SplitFilename(file.name); let rt_data: DecryptResult; switch (raw.ext) { + case 'mg3d': // Migu Wav + rt_data = await Mg3dDecrypt(file.raw, raw.name); + break; case 'ncm': // Netease Mp3/Flac rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext); break; diff --git a/src/decrypt/kgm.ts b/src/decrypt/kgm.ts index 9ac5ccf..ca3afa6 100644 --- a/src/decrypt/kgm.ts +++ b/src/decrypt/kgm.ts @@ -63,7 +63,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string) const mime = AudioMimeType[ext]; let musicBlob = new Blob([musicDecoded], { type: mime }); const musicMeta = await metaParseBlob(musicBlob); - const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artists == undefined ? musicMeta.common.artist : musicMeta.common.artists.toString()); + const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || "")); return { album: musicMeta.common.album, picture: GetCoverFromFile(musicMeta), diff --git a/src/decrypt/kwm.ts b/src/decrypt/kwm.ts index 4213f4b..a72b202 100644 --- a/src/decrypt/kwm.ts +++ b/src/decrypt/kwm.ts @@ -38,7 +38,7 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom let musicBlob = new Blob([audioData], { type: mime }); const musicMeta = await metaParseBlob(musicBlob); - const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artists == undefined ? musicMeta.common.artist : musicMeta.common.artists.toString()); + const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || "")); return { album: musicMeta.common.album, picture: GetCoverFromFile(musicMeta), diff --git a/src/decrypt/mg3d.ts b/src/decrypt/mg3d.ts new file mode 100644 index 0000000..8ddc845 --- /dev/null +++ b/src/decrypt/mg3d.ts @@ -0,0 +1,71 @@ +import { Decrypt as RawDecrypt } from './raw'; +import { GetArrayBuffer } from '@/decrypt/utils'; +import { DecryptResult } from '@/decrypt/entity'; + +const segmentSize = 0x20; + +function isPrintableAsciiChar(ch: number) { + return ch >= 0x20 && ch <= 0x7E; +} + +function isUpperHexChar(ch: number) { + return (ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x46); +} + +/** + * @param {Buffer} data + * @param {Buffer} key + * @param {boolean} copy + * @returns Buffer + */ +function decryptSegment(data: Uint8Array, key: Uint8Array) { + for (let i = 0; i < data.byteLength; i++) { + data[i] -= key[i % segmentSize]; + } + return Buffer.from(data); +} + +export async function Decrypt(file: File, raw_filename: string): Promise { + const buf = new Uint8Array(await GetArrayBuffer(file)); + + // 咪咕编码的 WAV 文件有很多“空洞”内容,尝试密钥。 + const header = buf.slice(0, 0x100); + const bytesRIFF = Buffer.from('RIFF', 'ascii'); + const bytesWaveFormat = Buffer.from('WAVEfmt ', 'ascii'); + const possibleKeys = []; + + for (let i = segmentSize; i < segmentSize * 20; i += segmentSize) { + const possibleKey = buf.slice(i, i + segmentSize); + if (!possibleKey.every(isUpperHexChar)) continue; + + const tempHeader = decryptSegment(header, possibleKey); + if (tempHeader.slice(0, 4).compare(bytesRIFF)) continue; + if (tempHeader.slice(8, 16).compare(bytesWaveFormat)) continue; + + // fmt chunk 大小可以是 16 / 18 / 40。 + const fmtChunkSize = tempHeader.readUInt32LE(0x10); + if (![16, 18, 40].includes(fmtChunkSize)) continue; + + // 下一个 chunk + const firstDataChunkOffset = 0x14 + fmtChunkSize; + const chunkName = tempHeader.slice(firstDataChunkOffset, firstDataChunkOffset + 4); + if (!chunkName.every(isPrintableAsciiChar)) continue; + + const secondDataChunkOffset = firstDataChunkOffset + 8 + tempHeader.readUInt32LE(firstDataChunkOffset + 4); + if (secondDataChunkOffset <= header.byteLength) { + const secondChunkName = tempHeader.slice(secondDataChunkOffset, secondDataChunkOffset + 4); + if (!secondChunkName.every(isPrintableAsciiChar)) continue; + } + + possibleKeys.push(Buffer.from(possibleKey).toString('ascii')); + } + + if (possibleKeys.length <= 0) { + throw new Error(`ERROR: no suitable key discovered`); + } + + const decryptionKey = Buffer.from(possibleKeys[0], 'ascii'); + decryptSegment(buf, decryptionKey); + const musicData = new Blob([buf], { type: 'audio/x-wav' }); + return await RawDecrypt(musicData, raw_filename, 'wav', false); +} diff --git a/src/decrypt/ncmcache.ts b/src/decrypt/ncmcache.ts index 58dd4ff..f6ef63f 100644 --- a/src/decrypt/ncmcache.ts +++ b/src/decrypt/ncmcache.ts @@ -13,7 +13,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) 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.artists == undefined ? tag.common.artist : tag.common.artists.toString()); + const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || "")); return { title, diff --git a/src/decrypt/qmccache.ts b/src/decrypt/qmccache.ts index cfa5ba5..59bf5c1 100644 --- a/src/decrypt/qmccache.ts +++ b/src/decrypt/qmccache.ts @@ -54,7 +54,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) throw '不支持的QQ音乐缓存格式'; } const tag = await metaParseBlob(audioBlob); - const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist)); + const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || "")); return { title, diff --git a/src/decrypt/raw.ts b/src/decrypt/raw.ts index 3ff01ba..4d2427d 100644 --- a/src/decrypt/raw.ts +++ b/src/decrypt/raw.ts @@ -17,7 +17,7 @@ export async function Decrypt( 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, String(tag.common.artists || tag.common.artist)); + const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || '')); return { title, diff --git a/src/decrypt/xm.ts b/src/decrypt/xm.ts index 1c487cf..afa9841 100644 --- a/src/decrypt/xm.ts +++ b/src/decrypt/xm.ts @@ -49,7 +49,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string) const { title, artist } = GetMetaFromFile( raw_filename, musicMeta.common.title, - String(musicMeta.common.artists || musicMeta.common.artist), + String(musicMeta.common.artists || musicMeta.common.artist || ""), raw_filename.indexOf('_') === -1 ? '-' : '_', ); diff --git a/src/utils/qm_meta.ts b/src/utils/qm_meta.ts index a5475a9..d93d44f 100644 --- a/src/utils/qm_meta.ts +++ b/src/utils/qm_meta.ts @@ -20,6 +20,8 @@ interface MetaResult { blob: Blob; } +const fromGBK = (text?: string) => iconv.decode(new Buffer(text || ''), 'gbk'); + /** * * @param musicBlob 音乐文件(解密后) @@ -41,14 +43,13 @@ export async function extractQQMusicMeta( console.warn('try using gbk encoding to decode meta'); musicMeta.common.artist = ''; if (!musicMeta.common.artists) { - musicMeta.common.artist = iconv.decode(new Buffer(musicMeta.common.artist ?? ''), 'gbk'); + musicMeta.common.artist = fromGBK(musicMeta.common.artist); } else { - musicMeta.common.artists.forEach((artist) => artist = iconv.decode(new Buffer(artist ?? ''), 'gbk')); - musicMeta.common.artist = musicMeta.common.artists.toString(); + musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join(); } - musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ''), 'gbk'); - musicMeta.common.album = iconv.decode(new Buffer(musicMeta.common.album ?? ''), 'gbk'); + musicMeta.common.title = fromGBK(musicMeta.common.title); + musicMeta.common.album = fromGBK(musicMeta.common.album); } }