diff --git a/package-lock.json b/package-lock.json index 35494f0..92dbbf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1881,6 +1881,38 @@ "pify": "^4.0.1" } }, + "babel-plugin-component": { + "version": "1.1.1", + "resolved": "https://registry.npm.taobao.org/babel-plugin-component/download/babel-plugin-component-1.1.1.tgz", + "integrity": "sha1-mwI6I/9cmq4P1WxaGLnKuMTUXuo=", + "dev": true, + "requires": { + "@babel/helper-module-imports": "7.0.0-beta.35" + }, + "dependencies": { + "@babel/helper-module-imports": { + "version": "7.0.0-beta.35", + "resolved": "https://registry.npm.taobao.org/@babel/helper-module-imports/download/@babel/helper-module-imports-7.0.0-beta.35.tgz", + "integrity": "sha1-MI41DnMXUs200PBY3x1wSSXGTgo=", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.35", + "lodash": "^4.2.0" + } + }, + "@babel/types": { + "version": "7.0.0-beta.35", + "resolved": "https://registry.npm.taobao.org/@babel/types/download/@babel/types-7.0.0-beta.35.tgz", + "integrity": "sha1-z5M6mpo4SEynJLM1uI2Dcm1auWA=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.2.0", + "to-fast-properties": "^2.0.0" + } + } + } + }, "babel-plugin-dynamic-import-node": { "version": "2.3.0", "resolved": "https://registry.npm.taobao.org/babel-plugin-dynamic-import-node/download/babel-plugin-dynamic-import-node-2.3.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbabel-plugin-dynamic-import-node%2Fdownload%2Fbabel-plugin-dynamic-import-node-2.3.0.tgz", @@ -2173,6 +2205,11 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, + "browser-id3-writer": { + "version": "4.1.0", + "resolved": "https://registry.npm.taobao.org/browser-id3-writer/download/browser-id3-writer-4.1.0.tgz", + "integrity": "sha1-pL+ye82dpgHoqgPAvuJvovVuf0c=" + }, "browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npm.taobao.org/browserify-aes/download/browserify-aes-1.2.0.tgz", @@ -3092,6 +3129,11 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "3.1.9-1", + "resolved": "https://registry.npm.taobao.org/crypto-js/download/crypto-js-3.1.9-1.tgz", + "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npm.taobao.org/css-color-names/download/css-color-names-0.0.4.tgz", @@ -6046,6 +6088,14 @@ "integrity": "sha1-gFZNLkg9rPbo7yCWUKZ98/DCg6Q=", "dev": true }, + "jsmediatags": { + "version": "3.9.1", + "resolved": "https://registry.npm.taobao.org/jsmediatags/download/jsmediatags-3.9.1.tgz", + "integrity": "sha1-yPFsVd2Es0HbQvcNSbEMVTFM8X0=", + "requires": { + "xhr2": "^0.1.4" + } + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npm.taobao.org/json-parse-better-errors/download/json-parse-better-errors-1.0.2.tgz", @@ -10617,6 +10667,11 @@ "async-limiter": "~1.0.0" } }, + "xhr2": { + "version": "0.1.4", + "resolved": "https://registry.npm.taobao.org/xhr2/download/xhr2-0.1.4.tgz", + "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" + }, "xtend": { "version": "4.0.1", "resolved": "http://registry.npm.taobao.org/xtend/download/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index a812c2b..221fb33 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,11 @@ "build": "vue-cli-service build" }, "dependencies": { + "browser-id3-writer": "^4.1.0", "core-js": "^2.6.5", + "crypto-js": "^3.1.9-1", "element-ui": "^2.4.5", + "jsmediatags": "^3.9.1", "register-service-worker": "^1.6.2", "vue": "^2.6.10" }, @@ -16,6 +19,7 @@ "@vue/cli-plugin-babel": "^3.9.0", "@vue/cli-plugin-pwa": "^3.9.0", "@vue/cli-service": "^3.9.0", + "babel-plugin-component": "^1.1.1", "vue-cli-plugin-element": "^1.0.1", "vue-template-compiler": "^2.6.10" } diff --git a/public/index.html b/public/index.html index 1af85c8..9154ac7 100644 --- a/public/index.html +++ b/public/index.html @@ -1,17 +1,69 @@ - - + + - music-crack - - + 音乐解锁 - By IXarea + + + + + + +
+
-
- - + +
+
+ + diff --git a/public/manifest.json b/public/manifest.json index e828e3a..b3841ed 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,7 @@ { - "name": "music-crack", - "short_name": "music-crack", + "name": "音乐解锁 - By IXarea", + "short_name": "音乐解锁", + "description": "在任何设备上解锁已购的加密音乐!支持QQ音乐与网易云音乐!", "icons": [ { "src": "./img/icons/android-chrome-192x192.png", diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index eb05362..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: diff --git a/src/App.vue b/src/App.vue index 440dc7e..6455391 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,36 +1,227 @@ diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index d67435e..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - diff --git a/src/main.js b/src/main.js index 088cdec..fc70151 100644 --- a/src/main.js +++ b/src/main.js @@ -3,8 +3,9 @@ import App from './App.vue' import './registerServiceWorker' import './plugins/element.js' -Vue.config.productionTip = false +// only if your build system can import css, otherwise import it wherever you would import your css. +Vue.config.productionTip = false; new Vue({ - render: h => h(App), -}).$mount('#app') + render: h => h(App), +}).$mount('#app'); diff --git a/src/plugins/element.js b/src/plugins/element.js index c48a6ef..6df0b24 100644 --- a/src/plugins/element.js +++ b/src/plugins/element.js @@ -1,5 +1,33 @@ import Vue from 'vue' -import Element from 'element-ui' +import { + Image, + Button, + Table, + TableColumn, + Main, + Footer, + Container, + Icon, + Row, + Col, + Upload, + Notification, + Link +} from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css' -Vue.use(Element) +Vue.use(Link); +Vue.use(Image); +Vue.use(Button); +Vue.use(Table); +Vue.use(TableColumn); +Vue.use(Main); +Vue.use(Footer); +Vue.use(Container); +Vue.use(Icon); +Vue.use(Row); +Vue.use(Col); +Vue.use(Upload); +Vue.prototype.$notify = Notification; + + diff --git a/src/plugins/ncm.js b/src/plugins/ncm.js new file mode 100644 index 0000000..e36fac0 --- /dev/null +++ b/src/plugins/ncm.js @@ -0,0 +1,165 @@ +const CryptoJS = require("crypto-js"); +const CORE_KEY = CryptoJS.enc.Hex.parse("687a4852416d736f356b496e62617857"); +const META_KEY = CryptoJS.enc.Hex.parse("2331346C6A6B5F215C5D2630553C2728"); + +const audio_mime_type = { + mp3: "audio/mpeg", + flac: "audio/flac" +}; + + +export {Decrypt}; + +async function Decrypt(file) { + + const fileBuffer = await new Promise(reslove => { + const reader = new FileReader(); + reader.onload = (e) => { + reslove(e.target.result); + }; + reader.readAsArrayBuffer(file); + }); + + const dataView = new DataView(fileBuffer); + + if (dataView.getUint32(0, true) !== 0x4e455443 || + dataView.getUint32(4, true) !== 0x4d414446 + ) { + console.log({type: "error", data: "not ncm file"}); + return; + } + + let offset = 10; + + const keyData = (() => { + const keyLen = dataView.getUint32(offset, true); + offset += 4; + const cipherText = new Uint8Array(fileBuffer, offset, keyLen).map( + uint8 => uint8 ^ 0x64 + ); + offset += keyLen; + + const plainText = CryptoJS.AES.decrypt( + {ciphertext: CryptoJS.lib.WordArray.create(cipherText)}, + CORE_KEY, + { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.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; + } + } + + return result.slice(17); + })(); + + const keyBox = (() => { + const box = new Uint8Array(Array(256).keys()); + + const keyDataLen = keyData.length; + + let j = 0; + + for (let i = 0; i < 256; i++) { + j = (box[i] + j + keyData[i % keyDataLen]) & 0xff; + [box[i], box[j]] = [box[j], box[i]]; + } + + return box.map((_, i, arr) => { + i = (i + 1) & 0xff; + const si = arr[i]; + const sj = arr[(i + si) & 0xff]; + return arr[(si + sj) & 0xff]; + }); + })(); + + /** + * @typedef {Object} MusicMetaType + * @property {Number} musicId + * @property {String} musicName + * @property {[[String, Number]]} artist + * @property {String} album + * @property {"flac"|"mp3"} format + * @property {String} albumPic + */ + + /** @type {MusicMetaType|undefined} */ + const musicMeta = (() => { + const metaDataLen = dataView.getUint32(offset, true); + offset += 4; + if (metaDataLen === 0) { + return {}; + } + + const cipherText = new Uint8Array(fileBuffer, offset, metaDataLen).map( + data => data ^ 0x63 + ); + offset += metaDataLen; + + const plainText = CryptoJS.AES.decrypt( + { + ciphertext: CryptoJS.enc.Base64.parse( + CryptoJS.lib.WordArray.create(cipherText.slice(22)).toString(CryptoJS.enc.Utf8) + ) + }, + META_KEY, + {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7} + ); + + const result = JSON.parse(plainText.toString(CryptoJS.enc.Utf8).slice(6)); + result.albumPic = result.albumPic.replace("http:", "https:"); + return result; + })(); + + offset += dataView.getUint32(offset + 5, true) + 13; + + const audioData = new Uint8Array(fileBuffer, offset); + const audioDataLen = audioData.length; + + + for (let cur = 0; cur < audioDataLen; ++cur) { + audioData[cur] ^= keyBox[cur & 0xff]; + } + + + if (musicMeta.format === undefined) { + musicMeta.format = (() => { + const [f, L, a, C] = audioData; + if (f === 0x66 && L === 0x4c && a === 0x61 && C === 0x43) { + return "flac"; + } + return "mp3"; + })(); + } + const mime = audio_mime_type[musicMeta.format]; + const musicData = new Blob([audioData], { + type: mime + }); + + const musicUrl = URL.createObjectURL(musicData); + + const artists = []; + musicMeta.artist.forEach(arr => { + artists.push(arr[0]); + }); + const filename = artists.join(" & ") + " - " + musicMeta.musicName + "." + musicMeta.format; + return { + meta: musicMeta, + file: musicUrl, + picture: musicMeta.albumPic, + title: musicMeta.musicName, + album: musicMeta.album, + artist: artists.join(" & "), + filename: filename, + mime: mime + }; +} + diff --git a/src/plugins/qmc.js b/src/plugins/qmc.js new file mode 100644 index 0000000..4530352 --- /dev/null +++ b/src/plugins/qmc.js @@ -0,0 +1,125 @@ +const jsmediatags = require("jsmediatags"); +export {Decrypt} +const SEED_MAP = [ + [0x4a, 0xd6, 0xca, 0x90, 0x67, 0xf7, 0x52], + [0x5e, 0x95, 0x23, 0x9f, 0x13, 0x11, 0x7e], + [0x47, 0x74, 0x3d, 0x90, 0xaa, 0x3f, 0x51], + [0xc6, 0x09, 0xd5, 0x9f, 0xfa, 0x66, 0xf9], + [0xf3, 0xd6, 0xa1, 0x90, 0xa0, 0xf7, 0xf0], + [0x1d, 0x95, 0xde, 0x9f, 0x84, 0x11, 0xf4], + [0x0e, 0x74, 0xbb, 0x90, 0xbc, 0x3f, 0x92], + [0x00, 0x09, 0x5b, 0x9f, 0x62, 0x66, 0xa1]]; +const audio_mime_type = { + mp3: "audio/mpeg", + flac: "audio/flac" +}; + +async function Decrypt(file) { + // 获取扩展名 + let filename_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase(); + let new_ext; + switch (filename_ext) { + case "qmc0": + case "qmc3": + new_ext = "mp3"; + break; + case "qmcflac": + new_ext = "flac"; + break; + default: + return; + } + const mime = audio_mime_type[new_ext]; + // 读取文件 + const fileBuffer = await new Promise(reslove => { + const reader = new FileReader(); + reader.onload = (e) => { + reslove(e.target.result); + }; + reader.readAsArrayBuffer(file); + }); + const audioData = new Uint8Array(fileBuffer); + const audioDataLen = audioData.length; + // 转换数据 + const seed = new Mask(); + for (let cur = 0; cur < audioDataLen; ++cur) { + audioData[cur] ^= seed.NextMask(); + } + // 导出 + const musicData = new Blob([audioData], { + type: mime + }); + const musicUrl = URL.createObjectURL(musicData); + // 读取Meta + let tag = await new Promise(resolve => { + new jsmediatags.Reader(musicData).read({ + onSuccess: resolve, + onError: (err) => { + console.log(err); + resolve({tags: {}}) + } + }); + }); + + // 处理无标题歌手 + let filename_array = file.name.substring(0, file.name.lastIndexOf(".")).split("-"); + let title = tag.tags.title; + let artist = tag.tags.artist; + if (filename_array.length > 1) { + if (artist === undefined) artist = filename_array[0].trim(); + if (title === undefined) title = filename_array[1].trim(); + } else if (filename_array.length === 1) { + if (title === undefined) title = filename_array[0].trim(); + } + const filename = artist + " - " + title + "." + new_ext; + // 处理无封面 + let pic_url = ""; + if (tag.tags.picture !== undefined) { + let pic = new Blob([new Uint8Array(tag.tags.picture.data)], {type: tag.tags.picture.format}); + pic_url = URL.createObjectURL(pic); + } + // 返回 + return { + filename: filename, + title: title, + artist: artist, + album: tag.tags.album, + file: musicUrl, + picture: pic_url, + mime: mime + } +} + +class Mask { + constructor() { + this.x = -1; + this.y = 8; + this.dx = 1; + this.index = -1; + } + + NextMask() { + let ret; + this.index++; + if (this.x < 0) { + this.dx = 1; + this.y = (8 - this.y) % 8; + ret = 0xc3 + } else if (this.x > 6) { + this.dx = -1; + this.y = 7 - this.y; + ret = 0xd8 + } else { + ret = SEED_MAP[this.y][this.x] + } + this.x += this.dx; + if (this.index === 0x8000 || (this.index > 0x8000 && (this.index + 1) % 0x8000 === 0)) { + return this.NextMask() + } + return ret + } + + +} + + diff --git a/src/plugins/raw.js b/src/plugins/raw.js new file mode 100644 index 0000000..bbafbdd --- /dev/null +++ b/src/plugins/raw.js @@ -0,0 +1,51 @@ +const jsmediatags = require("jsmediatags"); +export {Decrypt} + +const audio_mime_type = { + mp3: "audio/mpeg", + flac: "audio/flac" +}; + +async function Decrypt(file) { + let tag = await new Promise(resolve => { + new jsmediatags.Reader(file).read({ + onSuccess: resolve, + onError: () => { + resolve({tags: {}}) + } + }); + }); + let pic_url = ""; + if (tag.tags.picture !== undefined) { + let pic = new Blob([new Uint8Array(tag.tags.picture.data)], {type: tag.tags.picture.format}); + pic_url = URL.createObjectURL(pic); + } + + let file_url = URL.createObjectURL(file); + + + let filename_no_ext = file.name.substring(0, file.name.lastIndexOf(".")); + let filename_array = filename_no_ext.split("-"); + let filename_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase(); + const mime = audio_mime_type[filename_ext]; + let title = tag.tags.title; + let artist = tag.tags.artist; + + if (filename_array.length > 1) { + if (artist === undefined) artist = filename_array[0].trim(); + if (title === undefined) title = filename_array[1].trim(); + } else if (filename_array.length === 1) { + if (title === undefined) title = filename_array[0].trim(); + } + + const filename = artist + " - " + title + "." + filename_ext; + return { + filename: filename, + title: title, + artist: artist, + album: tag.tags.album, + picture: pic_url, + file: file_url, + mime: mime + } +} \ No newline at end of file diff --git a/vue.config.js b/vue.config.js new file mode 100644 index 0000000..e3188e2 --- /dev/null +++ b/vue.config.js @@ -0,0 +1,4 @@ +module.exports = { + publicPath: '/music/', + productionSourceMap: false +}; \ No newline at end of file