diff --git a/package-lock.json b/package-lock.json index 23beffc..d6cd297 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.5", "@jixun/qmc2-crypto": "^0.0.5-R4", + "@unlock-music-gh/joox-crypto": "^0.0.1-R2", "base64-js": "^1.5.1", "browser-id3-writer": "^4.4.0", "core-js": "^3.16.0", @@ -3485,6 +3486,17 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, + "node_modules/@unlock-music-gh/joox-crypto": { + "version": "0.0.1-R3", + "resolved": "https://registry.npmjs.org/@unlock-music-gh/joox-crypto/-/joox-crypto-0.0.1-R3.tgz", + "integrity": "sha512-zZRiDXKI5SxuBIcW/rsGL8jNvyWxtA5cNRfg69WcsZK2DqztY8M2q1kMe96MP1AyM+cKpNQ50jAKh77VdFv9rA==", + "dependencies": { + "crypto-js": "^4.1.1" + }, + "bin": { + "joox-decrypt": "joox-decrypt" + } + }, "node_modules/@vue/babel-helper-vue-jsx-merge-props": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz", @@ -23622,6 +23634,14 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, + "@unlock-music-gh/joox-crypto": { + "version": "0.0.1-R3", + "resolved": "https://registry.npmjs.org/@unlock-music-gh/joox-crypto/-/joox-crypto-0.0.1-R3.tgz", + "integrity": "sha512-zZRiDXKI5SxuBIcW/rsGL8jNvyWxtA5cNRfg69WcsZK2DqztY8M2q1kMe96MP1AyM+cKpNQ50jAKh77VdFv9rA==", + "requires": { + "crypto-js": "^4.1.1" + } + }, "@vue/babel-helper-vue-jsx-merge-props": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz", diff --git a/package.json b/package.json index 99117a8..1431651 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.5", "@jixun/qmc2-crypto": "^0.0.5-R4", + "@unlock-music-gh/joox-crypto": "^0.0.1-R2", "base64-js": "^1.5.1", "browser-id3-writer": "^4.4.0", "core-js": "^3.16.0", diff --git a/src/component/ConfigDialog.vue b/src/component/ConfigDialog.vue new file mode 100644 index 0000000..e0a05ce --- /dev/null +++ b/src/component/ConfigDialog.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/decrypt/common.ts b/src/decrypt/common.ts index ec7c935..03d3a39 100644 --- a/src/decrypt/common.ts +++ b/src/decrypt/common.ts @@ -7,6 +7,7 @@ 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 { Decrypt as JooxDecrypt } from '@/decrypt/joox'; import { DecryptResult, FileInfo } from '@/decrypt/entity'; import { SplitFilename } from '@/decrypt/utils'; @@ -68,6 +69,9 @@ export async function CommonDecrypt(file: FileInfo): Promise { case 'kgma': rt_data = await KgmDecrypt(file.raw, raw.name, raw.ext); break; + case 'ofl_en': + rt_data = await JooxDecrypt(file.raw, raw.name, raw.ext); + break; default: throw '不支持此文件格式'; } diff --git a/src/decrypt/joox.ts b/src/decrypt/joox.ts new file mode 100644 index 0000000..a5fab16 --- /dev/null +++ b/src/decrypt/joox.ts @@ -0,0 +1,34 @@ +import { DecryptResult } from './entity'; +import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from './utils'; + +import jooxFactory from '@unlock-music-gh/joox-crypto'; +import storage from '@/utils/storage'; +import { MergeUint8Array } from '@/utils/MergeUint8Array'; + +export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise { + const uuid = await storage.loadJooxUUID(''); + if (!uuid || uuid.length !== 32) { + throw new Error('请在“解密设定”填写应用 Joox 应用的 UUID。'); + } + + const fileBuffer = new Uint8Array(await GetArrayBuffer(file)); + const decryptor = jooxFactory(fileBuffer, uuid); + if (!decryptor) { + throw new Error('不支持的 joox 加密格式'); + } + + const musicDecoded = MergeUint8Array(decryptor.decryptFile(fileBuffer)); + const ext = SniffAudioExt(musicDecoded); + const mime = AudioMimeType[ext]; + const musicBlob = new Blob([musicDecoded], { type: mime }); + + return { + title: raw_filename.replace(/\.[^\.]+$/, ''), + artist: '未知', + album: '未知', + file: URL.createObjectURL(musicBlob), + blob: musicBlob, + mime: mime, + ext: ext, + }; +} diff --git a/src/decrypt/qmc_wasm.ts b/src/decrypt/qmc_wasm.ts index 22e2c31..bec030e 100644 --- a/src/decrypt/qmc_wasm.ts +++ b/src/decrypt/qmc_wasm.ts @@ -1,4 +1,5 @@ import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle'; +import { MergeUint8Array } from '@/utils/MergeUint8Array'; // 检测文件末端使用的缓冲区大小 const DETECTION_SIZE = 40; @@ -6,22 +7,6 @@ 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 加密的文件。 * diff --git a/src/main.ts b/src/main.ts index 97845b2..b046b80 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,9 +6,13 @@ import { Checkbox, Col, Container, + Dialog, + Form, + FormItem, Footer, Icon, Image, + Input, Link, Main, Notification, @@ -26,6 +30,10 @@ import 'element-ui/lib/theme-chalk/base.css'; Vue.use(Link); Vue.use(Image); Vue.use(Button); +Vue.use(Dialog); +Vue.use(Form); +Vue.use(FormItem); +Vue.use(Input); Vue.use(Table); Vue.use(TableColumn); Vue.use(Main); diff --git a/src/utils/MergeUint8Array.ts b/src/utils/MergeUint8Array.ts new file mode 100644 index 0000000..29f91e0 --- /dev/null +++ b/src/utils/MergeUint8Array.ts @@ -0,0 +1,15 @@ +export 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; +} diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..74bfe1d --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,7 @@ +import BaseStorage from './storage/BaseStorage'; +import BrowserNativeStorage from './storage/BrowserNativeStorage'; +import ChromeExtensionStorage from './storage/ChromeExtensionStorage'; + +const storage: BaseStorage = ChromeExtensionStorage.works ? new ChromeExtensionStorage() : new BrowserNativeStorage(); + +export default storage; diff --git a/src/utils/storage/BaseStorage.ts b/src/utils/storage/BaseStorage.ts new file mode 100644 index 0000000..8a5dafc --- /dev/null +++ b/src/utils/storage/BaseStorage.ts @@ -0,0 +1,14 @@ +const KEY_JOOX_UUID = 'joox.uuid'; + +export default abstract class BaseStorage { + protected abstract save(name: string, value: T): Promise; + protected abstract load(name: string, defaultValue: T): Promise; + + public saveJooxUUID(uuid: string): Promise { + return this.save(KEY_JOOX_UUID, uuid); + } + + public loadJooxUUID(defaultValue: string = ''): Promise { + return this.load(KEY_JOOX_UUID, defaultValue); + } +} diff --git a/src/utils/storage/BrowserNativeStorage.ts b/src/utils/storage/BrowserNativeStorage.ts new file mode 100644 index 0000000..b17c048 --- /dev/null +++ b/src/utils/storage/BrowserNativeStorage.ts @@ -0,0 +1,15 @@ +import BaseStorage from './BaseStorage'; + +export default class BrowserNativeStorage extends BaseStorage { + protected async load(name: string, defaultValue: T): Promise { + const result = localStorage.getItem(name); + if (result === null) { + return defaultValue; + } + return JSON.parse(result); + } + + protected async save(name: string, value: T): Promise { + localStorage.setItem(name, JSON.stringify(value)); + } +} diff --git a/src/utils/storage/ChromeExtensionStorage.ts b/src/utils/storage/ChromeExtensionStorage.ts new file mode 100644 index 0000000..2338b89 --- /dev/null +++ b/src/utils/storage/ChromeExtensionStorage.ts @@ -0,0 +1,21 @@ +import BaseStorage from './BaseStorage'; + +declare var chrome: any; + +export default class ChromeExtensionStorage extends BaseStorage { + static get works(): boolean { + return Boolean(chrome?.storage?.local?.set); + } + + protected async load(name: string, defaultValue: T): Promise { + const result = await chrome.storage.local.get({ [name]: defaultValue }); + if (Object.prototype.hasOwnProperty.call(result, name)) { + return result[name]; + } + return defaultValue; + } + + protected async save(name: string, value: T): Promise { + return chrome.storage.local.set({ [name]: value }); + } +} diff --git a/src/view/Home.vue b/src/view/Home.vue index d457bc5..35b6a2f 100644 --- a/src/view/Home.vue +++ b/src/view/Home.vue @@ -10,6 +10,13 @@ + + +
+ 部分解密方案需要设定解密参数。 +
+ 解密设定 +
下载全部 清除全部 @@ -35,6 +42,8 @@