diff --git a/package-lock.json b/package-lock.json index 5885b6a..5fb74ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,11 @@ "": { "name": "unlock-music", "version": "v1.9.1", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/preset-typescript": "^7.16.5", + "@jixun/qmc2-crypto": "^0.0.5-R4", "base64-js": "^1.5.1", "browser-id3-writer": "^4.4.0", "core-js": "^3.16.0", @@ -33,6 +35,7 @@ "@vue/cli-service": "^4.5.13", "babel-plugin-component": "^1.1.1", "jest": "^27.4.5", + "patch-package": "^6.4.7", "sass": "^1.38.1", "sass-loader": "^10.2.0", "semver": "^7.3.5", @@ -2980,6 +2983,11 @@ "regenerator-runtime": "^0.13.3" } }, + "node_modules/@jixun/qmc2-crypto": { + "version": "0.0.5-R4", + "resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.5-R4.tgz", + "integrity": "sha512-4xGClhxMd1BL7UjE+fZr+a4GYkfEjwU216WZ89ouANwR8Q27PhQrra+msEvM4J/mBBCjv/x/eIcS67XBasHKUQ==" + }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -4176,6 +4184,12 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, "node_modules/abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -8563,6 +8577,28 @@ "node": ">=8" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "dependencies": { + "micromatch": "^4.0.2" + } + }, + "node_modules/find-yarn-workspace-root/node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -12945,6 +12981,15 @@ "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -14489,6 +14534,15 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -14692,6 +14746,76 @@ "node": ">=0.10.0" } }, + "node_modules/patch-package": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz", + "integrity": "sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==", + "dev": true, + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^7.0.1", + "is-ci": "^2.0.0", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.0", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^5.6.0", + "slash": "^2.0.0", + "tmp": "^0.0.33" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/patch-package/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -18194,6 +18318,18 @@ "node": "*" } }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -23019,6 +23155,11 @@ "regenerator-runtime": "^0.13.3" } }, + "@jixun/qmc2-crypto": { + "version": "0.0.5-R4", + "resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.5-R4.tgz", + "integrity": "sha512-4xGClhxMd1BL7UjE+fZr+a4GYkfEjwU216WZ89ouANwR8Q27PhQrra+msEvM4J/mBBCjv/x/eIcS67XBasHKUQ==" + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -24069,6 +24210,12 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -27597,6 +27744,27 @@ "path-exists": "^4.0.0" } }, + "find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "requires": { + "micromatch": "^4.0.2" + }, + "dependencies": { + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + } + } + }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -30882,6 +31050,15 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11" + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -32143,6 +32320,12 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -32314,6 +32497,60 @@ "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true }, + "patch-package": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.4.7.tgz", + "integrity": "sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ==", + "dev": true, + "requires": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^7.0.1", + "is-ci": "^2.0.0", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.0", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^5.6.0", + "slash": "^2.0.0", + "tmp": "^0.0.33" + }, + "dependencies": { + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -35216,6 +35453,15 @@ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 8545852..9d73f93 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "private": true, "scripts": { + "postinstall": "patch-package", "serve": "vue-cli-service serve", "build": "vue-cli-service build", "test": "jest", @@ -18,6 +19,7 @@ }, "dependencies": { "@babel/preset-typescript": "^7.16.5", + "@jixun/qmc2-crypto": "^0.0.5-R4", "base64-js": "^1.5.1", "browser-id3-writer": "^4.4.0", "core-js": "^3.16.0", @@ -41,6 +43,7 @@ "@vue/cli-service": "^4.5.13", "babel-plugin-component": "^1.1.1", "jest": "^27.4.5", + "patch-package": "^6.4.7", "sass": "^1.38.1", "sass-loader": "^10.2.0", "semver": "^7.3.5", diff --git a/patches/threads+1.7.0.patch b/patches/threads+1.7.0.patch new file mode 100644 index 0000000..970b334 --- /dev/null +++ b/patches/threads+1.7.0.patch @@ -0,0 +1,11 @@ +diff --git a/node_modules/threads/worker.mjs b/node_modules/threads/worker.mjs +index c53ac7d..619007b 100644 +--- a/node_modules/threads/worker.mjs ++++ b/node_modules/threads/worker.mjs +@@ -1,4 +1,5 @@ +-import WorkerContext from "./dist/worker/index.js" ++// Workaround: use of import seems to break minifier. ++const WorkerContext = require("./dist/worker/index.js") + + export const expose = WorkerContext.expose + export const registerSerializer = WorkerContext.registerSerializer diff --git a/src/decrypt/common.ts b/src/decrypt/common.ts index 636e6f8..1be5e56 100644 --- a/src/decrypt/common.ts +++ b/src/decrypt/common.ts @@ -38,8 +38,10 @@ export async function CommonDecrypt(file: FileInfo): Promise { case "tkm"://QQ Music Accompaniment M4a case "bkcmp3"://Moo Music Mp3 case "bkcflac"://Moo Music Flac - case "mflac"://QQ Music Desktop Flac - case "mgg": //QQ Music Desktop Ogg + 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 diff --git a/src/decrypt/qmc.ts b/src/decrypt/qmc.ts index 7d6366a..e7fffb9 100644 --- a/src/decrypt/qmc.ts +++ b/src/decrypt/qmc.ts @@ -1,4 +1,4 @@ -import {QmcMask, QmcMaskDetectMflac, QmcMaskDetectMgg, QmcMaskGetDefault} from "./qmcMask"; +import {QmcMask, QmcMaskGetDefault} from "./qmcMask"; import {toByteArray as Base64Decode} from 'base64-js' import { AudioMimeType, @@ -9,6 +9,7 @@ import { SniffAudioExt, WriteMetaToFlac, WriteMetaToMp3 } from "@/decrypt/utils"; import {parseBlob as metaParseBlob} from "music-metadata-browser"; +import {DecryptQMCv2} from "./qmcv2"; import iconv from "iconv-lite"; @@ -17,51 +18,57 @@ import {queryAlbumCover, queryKeyInfo, reportKeyUsage} from "@/utils/api"; interface Handler { ext: string - detect: boolean - - handler(data?: Uint8Array): QmcMask | undefined + version: number } export const HandlerMap: { [key: string]: Handler } = { - "mgg": {handler: QmcMaskDetectMgg, ext: "ogg", detect: true}, - "mflac": {handler: QmcMaskDetectMflac, ext: "flac", detect: true}, - "mgg.cache": {handler: QmcMaskDetectMgg, ext: "ogg", detect: false}, - "mflac.cache": {handler: QmcMaskDetectMflac, ext: "flac", detect: false}, - "qmc0": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, - "qmc2": {handler: QmcMaskGetDefault, ext: "ogg", detect: false}, - "qmc3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, - "qmcogg": {handler: QmcMaskGetDefault, ext: "ogg", detect: false}, - "qmcflac": {handler: QmcMaskGetDefault, ext: "flac", detect: false}, - "bkcmp3": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, - "bkcflac": {handler: QmcMaskGetDefault, ext: "flac", detect: false}, - "tkm": {handler: QmcMaskGetDefault, ext: "m4a", detect: false}, - "666c6163": {handler: QmcMaskGetDefault, ext: "flac", detect: false}, - "6d7033": {handler: QmcMaskGetDefault, ext: "mp3", detect: false}, - "6f6767": {handler: QmcMaskGetDefault, ext: "ogg", detect: false}, - "6d3461": {handler: QmcMaskGetDefault, ext: "m4a", detect: false}, - "776176": {handler: QmcMaskGetDefault, ext: "wav", detect: false} + "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}, + + "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; - const fileData = new Uint8Array(await GetArrayBuffer(file)); - let audioData, seed, keyData; - if (handler.detect) { - const keyLen = new DataView(fileData.slice(fileData.length - 4).buffer).getUint32(0, true) - const keyPos = fileData.length - 4 - keyLen; - audioData = fileData.slice(0, keyPos); - seed = handler.handler(audioData); - keyData = fileData.slice(keyPos, keyPos + keyLen); - if (!seed) seed = await queryKey(keyData, raw_filename, raw_ext); - if (!seed) throw raw_ext + "格式仅提供实验性支持"; - } else { - audioData = fileData; - seed = handler.handler(audioData) as QmcMask; - if (!seed) throw raw_ext + "格式仅提供实验性支持"; + const fileBuffer = await GetArrayBuffer(file); + let musicDecoded: Uint8Array|undefined; + + if (version === 2) { + const v2Decrypted = await DecryptQMCv2(fileBuffer); + // 如果 v2 检测失败,降级到 v1 再尝试一次 + if (v2Decrypted) { + musicDecoded = v2Decrypted; + } else { + version = 1; + } + } + + if (version === 1) { + const seed = QmcMaskGetDefault(); + musicDecoded = seed.Decrypt(new Uint8Array(fileBuffer)); + } else if (!musicDecoded) { + throw new Error(`解密失败: ${raw_ext}`); } - let musicDecoded = seed.Decrypt(audioData); const ext = SniffAudioExt(musicDecoded, handler.ext); const mime = AudioMimeType[ext]; @@ -80,8 +87,6 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) } const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist) - if (keyData) reportKeyUsage(keyData, seed.getMatrix128(), - raw_filename, raw_ext, info.title, info.artist, musicMeta.common.album).then().catch(); let imgUrl = GetCoverFromFile(musicMeta); if (!imgUrl) { diff --git a/src/decrypt/qmcMask.ts b/src/decrypt/qmcMask.ts index 54af4bf..161ddd0 100644 --- a/src/decrypt/qmcMask.ts +++ b/src/decrypt/qmcMask.ts @@ -1,31 +1,3 @@ -import {BytesHasPrefix, FLAC_HEADER, OGG_HEADER} from "@/decrypt/utils"; - -const QMOggPublicHeader1 = [ - 0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, - 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x1e, 0x01, 0x76, 0x6f, 0x72, - 0x62, 0x69, 0x73, 0x00, 0x00, 0x00, 0x00, 0x02, 0x44, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xee, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x01, 0x4f, 0x67, 0x67, 0x53, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff]; -const QMOggPublicHeader2 = [ - 0x03, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x2c, 0x00, 0x00, 0x00, 0x58, 0x69, 0x70, 0x68, 0x2e, - 0x4f, 0x72, 0x67, 0x20, 0x6c, 0x69, 0x62, 0x56, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x20, 0x49, 0x20, - 0x32, 0x30, 0x31, 0x35, 0x30, 0x31, 0x30, 0x35, 0x20, 0x28, 0xe2, 0x9b, 0x84, 0xe2, 0x9b, 0x84, - 0xe2, 0x9b, 0x84, 0xe2, 0x9b, 0x84, 0x29, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x54, - 0x49, 0x54, 0x4c, 0x45, 0x3d]; -const QMOggPublicConf1 = [ - 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, - 0, 0, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 6, 3, 3, 3, 3, 6, 6, 6, 6, - 3, 3, 3, 3, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9, - 0, 0, 0, 0]; -const QMOggPublicConf2 = [ - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 0, 1, 3, 3, 0, 1, 3, 3, 3, - 3, 3, 3, 3, 3]; const QMCDefaultMaskMatrix = [ 0xde, 0x51, 0xfa, 0xc3, 0x4a, 0xd6, 0xca, 0x90, 0x7e, 0x67, 0x5e, 0xf7, 0xd5, 0x52, 0x84, 0xd8, @@ -34,7 +6,6 @@ const QMCDefaultMaskMatrix = [ 0x1d, 0x3f, 0x5b, 0xf0, 0x13, 0x0e, 0x09, 0x3d, 0xf9, 0xbc, 0x00, 0x11]; - const AllMapping: number[][] = []; const Mask128to44: number[] = []; @@ -57,7 +28,6 @@ const Mask128to44: number[] = []; }) })(); - export class QmcMask { private readonly Matrix128: number[]; @@ -126,81 +96,3 @@ export class QmcMask { export function QmcMaskGetDefault() { return new QmcMask(QMCDefaultMaskMatrix) } - -export function QmcMaskDetectMflac(data: Uint8Array) { - let search_len = Math.min(0x8000, data.length) - for (let block_idx = 0; block_idx < search_len; block_idx += 128) { - try { - let mask = new QmcMask(data.slice(block_idx, block_idx + 128)); - if (BytesHasPrefix(mask.Decrypt(data.slice(0, FLAC_HEADER.length)), FLAC_HEADER)) { - return mask - } - } catch (e) { - } - } - return -} - -export function QmcMaskDetectMgg(data: Uint8Array) { - if (data.length < 0x100) return - let matrixConfidence: { [key: number]: { [key: number]: number } } = {}; - for (let i = 0; i < 44; i++) matrixConfidence[i] = {}; - - const page2 = data[0x54] ^ data[0xC] ^ QMOggPublicHeader1[0xC]; - const spHeader = QmcGenerateOggHeader(page2) - const spConf = QmcGenerateOggConf(page2) - - for (let idx128 = 0; idx128 < spHeader.length; idx128++) { - if (spConf[idx128] === 0) continue; - let idx44 = Mask128to44[idx128 % 128]; - let _m = data[idx128] ^ spHeader[idx128] - let confidence = spConf[idx128]; - if (_m in matrixConfidence[idx44]) { - matrixConfidence[idx44][_m] += confidence - } else { - matrixConfidence[idx44][_m] = confidence - } - } - let matrix = []; - try { - for (let i = 0; i < 44; i++) - matrix[i] = calcMaskFromConfidence(matrixConfidence[i]); - } catch (e) { - return - } - const mask = new QmcMask(matrix); - if (BytesHasPrefix(mask.Decrypt(data.slice(0, OGG_HEADER.length)), OGG_HEADER)) { - return mask - } - return -} - - -function calcMaskFromConfidence(confidence: { [key: number]: number }) { - const count = Object.keys(confidence).length - if (count === 0) throw "can not match at least one key"; - if (count > 1) console.warn("There are 2 potential value for the mask!") - let result = "" - let conf = 0 - for (let idx in confidence) { - if (confidence[idx] > conf) { - result = idx; - conf = confidence[idx]; - } - } - return Number(result) -} - -function QmcGenerateOggHeader(page2: number) { - let spec = [page2, 0xFF] - for (let i = 2; i < page2; i++) spec.push(0xFF) - spec.push(0xFF) - return QMOggPublicHeader1.concat(spec, QMOggPublicHeader2) -} - -function QmcGenerateOggConf(page2: number) { - let specConf = [6, 0] - for (let i = 2; i < page2; i++) specConf.push(4) - specConf.push(0) - return QMOggPublicConf1.concat(specConf, QMOggPublicConf2) -} diff --git a/src/decrypt/qmcv2.ts b/src/decrypt/qmcv2.ts new file mode 100644 index 0000000..36f4b54 --- /dev/null +++ b/src/decrypt/qmcv2.ts @@ -0,0 +1,103 @@ +import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle'; + +// 检测文件末端使用的缓冲区大小 +const DETECTION_SIZE = 40; + +// 每次处理 2M 的数据 +const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024; + +function MergeUint8Array(array: Uint8Array[]): Uint8Array { + let length = 0; + array.forEach(item => { + length += item.length; + }); + + let mergedArray = new Uint8Array(length); + let offset = 0; + array.forEach(item => { + mergedArray.set(item, offset); + offset += item.length; + }); + + return mergedArray; +} + +/** + * 解密一个 QMC2 加密的文件。 + * + * 如果检测并解密成功,返回解密后的 Uint8Array 数据。 + * @param {ArrayBuffer} mggBlob 读入的文件 Blob + * @param {string} name 文件名 + * @return {Promise} + */ +export async function DecryptQMCv2(mggBlob: ArrayBuffer) { + // 初始化模组 + const QMCCrypto = await QMCCryptoModule(); + + // 申请内存块,并文件末端数据到 WASM 的内存堆 + const detectionBuf = new Uint8Array(mggBlob.slice(-DETECTION_SIZE)); + const pDetectionBuf = QMCCrypto._malloc(detectionBuf.length); + QMCCrypto.writeArrayToMemory(detectionBuf, pDetectionBuf); + + // 检测结果内存块 + const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection()); + + // 进行检测 + 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"); + + // 释放内存 + QMCCrypto._free(pDetectionBuf); + QMCCrypto._free(pDetectionResult); + + if (!detectOK) { + return false; + } + + // 计算解密后文件的大小。 + // 之前得到的 position 为相对当前检测数据起点的偏移。 + const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position; + + // 提取嵌入到文件的 EKey + const ekey = new Uint8Array( + mggBlob.slice(decryptedSize, decryptedSize + len) + ); + + // 解码 UTF-8 数据到 string + const decoder = new TextDecoder(); + const ekey_b64 = decoder.decode(ekey); + + // 初始化加密与缓冲区 + const hCrypto = QMCCrypto.createInstWidthEKey(ekey_b64); + const buf = QMCCrypto._malloc(DECRYPTION_BUF_SIZE); + + const decryptedParts = []; + let offset = 0; + let bytesToDecrypt = decryptedSize; + while (bytesToDecrypt > 0) { + const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE); + + // 解密一些片段 + 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)); + + offset += blockSize; + bytesToDecrypt -= blockSize; + } + QMCCrypto._free(buf); + hCrypto.delete(); + + return MergeUint8Array(decryptedParts); +}