Compare commits

...

175 Commits

Author SHA1 Message Date
Unlock Music Dev 4fec47201e Merge pull request 'Fix STag Filtering & README improvement & Update Supported Extensions' (#28) from xhacker-zzz/web:master into master
Reviewed-on: um/web#28
Reviewed-by: jixunmoe <jixunmoe@noreply.unlock-music.dev>
Reviewed-by: Unlock Music Dev <dev@unlock-music.dev>
2023-01-19 16:57:53 +00:00
xhacker-zzz 6d8389ce19 update readme 2023-01-19 19:41:52 +08:00
xhacker-zzz 8e361691bc update copyright year 2023-01-19 19:41:22 +08:00
xhacker-zzz c48532121d small fixes 2023-01-19 19:41:01 +08:00
Unlock Music Dev 0458ee469b Merge pull request 'New Decryptor: Add js decoder for Ximalaya X2M and X3M' (#23) from Yuki1536/um-web1:master into master
Reviewed-on: um/web#23
Reviewed-by: jixunmoe <jixunmoe@noreply.unlock-music.dev>
Reviewed-by: Unlock Music Dev <dev@unlock-music.dev>
2022-12-22 16:24:42 +00:00
Yuki1536 62d83c752e
Format with prettier. 2022-12-22 22:20:34 +09:00
Yuki1536 3393bb0bff
Change type 2022-12-16 20:34:13 +09:00
Yuki1536 d21c48caff
Small fixes 2022-12-15 12:08:12 +09:00
Yuki1536 b632c8fbc9
Update README.md 2022-12-12 20:35:15 +09:00
Yuki1536 48a497af19
Update README.md / Now it's 2022! 2022-12-12 19:58:36 +09:00
Yuki1536 2ae08c53fd
Store table u16array instead of bytes 2022-12-12 19:19:06 +09:00
Yuki1536 105023def7
Remove iconv-lite calling 2022-12-11 18:22:47 +09:00
Yuki1536 1fdab13ff8
Add x2m x3m decryptor 2022-12-11 17:54:07 +09:00
Yuki1536 1db42a10ff
Test: x2m x3m decrypt 2022-12-11 17:24:14 +09:00
Unlock Music Dev 47b7273dbc Merge pull request 'fix(kwm): allow new magic header on unlock-music web edition' (#22) from Yuki1536/um-web:master into master
Reviewed-on: um/web#22
Reviewed-by: um-dev <dev@unlock-music.dev>
2022-12-05 03:12:36 +00:00
Yuki1536 7e62def0c2
fix(kwm): allow new magic header 2022-12-05 10:26:41 +09:00
jixunmoe fe3bad5875 Merge pull request 'feat: added support for mmp4 extension' (#20) from jixunmoe/um-web:feat/qmc-new-ext-mmp4 into master
Reviewed-on: um/web#20
2022-11-27 14:34:07 +00:00
Jixun eb3091052e feat: added support for mmp4 extension 2022-11-27 14:12:17 +00:00
Unlock Music Dev a5bd0a312c Merge pull request 'chore: remove stats' (#18) from remove/stats into master
Reviewed-on: um/web#18
2022-11-27 02:03:21 +00:00
Unlock Music Dev 38f9b1b19a
chore: remove stats 2022-11-27 09:23:34 +08:00
jixunmoe 025b19d5cd Merge pull request 'docs: update link to artifact + ci badge' (#17) from readme/ci-link-and-badge into master
Reviewed-on: um/web#17
2022-11-27 00:56:21 +00:00
jixunmoe 551d3abd20 docs: update link to artifact + ci badge 2022-11-27 00:56:09 +00:00
jixunmoe 7671127311 Merge pull request 'ci: upload to gitea packages instead of s3' (#16) from jixunmoe/um-web:ci/use-gitea-packages into master
Reviewed-on: um/web#16
2022-11-27 00:48:19 +00:00
Jixun 70c78772ec ci: use same node image to skip dependency install. 2022-11-27 00:45:15 +00:00
Jixun 26e50473f2 ci: skip upload step when api key is empty (usually happens for PR) 2022-11-27 00:43:37 +00:00
Jixun 1fc7142957 ci: use emsdk instead of download emsdk each build. 2022-11-27 00:41:44 +00:00
Jixun ff06c81735 ci: specify debian image + set dependency 2022-11-27 00:26:21 +00:00
Jixun 9b76ef2959 ci: mark upload script as executable 2022-11-27 00:17:41 +00:00
Jixun d733dba221 ci: upload to gitea packages instead of s3 2022-11-26 23:52:48 +00:00
jixunmoe 8bc4b1d109 Merge pull request '使用 DroneCI 自动构建' (#13) from ci/build-with-drone into master
Reviewed-on: um/web#13
2022-11-26 21:05:39 +00:00
Jixun 278104644c ci: better glob for file upload 2022-11-26 20:59:08 +00:00
Jixun ab2b37ba3c chore: gitignore sha256sum.txt 2022-11-26 20:34:04 +00:00
Jixun 3c79a1f6e2 fix: syntax error for compare text in shell script 2022-11-26 20:21:43 +00:00
Jixun e79728c687 fix: drone build script 2022-11-26 20:11:13 +00:00
Jixun 9b3dfc0ed6 fix: keep dist after legacy has built 2022-11-26 20:11:05 +00:00
Jixun c5adc9dc05 chore: add nvmrc 2022-11-26 20:02:56 +00:00
Jixun b480088bb6 chore: don't prefix version string with "v" in package.json. 2022-11-26 20:02:52 +00:00
Jixun Wu 49b31301cf ci: initial experiment with ci build 2022-11-26 19:59:26 +00:00
Jixun Wu 892fea509f test: remove invalid test and avoid jest pick up tests from emsdk folder. 2022-11-26 19:21:30 +00:00
Jixun Wu 38cc5e2189 chore: share the same emsdk path. 2022-11-26 19:08:08 +00:00
Unlock Music Dev e569719f97 Merge pull request '其它: 移除主页说明失效的链接、更名 `.github` 到 `.gitlab`、加入 issue 模板。' (#8) from jixunmoe/um-web:docs/cleanup-readme into master
Reviewed-on: um/web#8
2022-11-24 18:33:55 +00:00
Jixun Wu 6bf8acb170 Merge remote-tracking branch 'upstream/master' into docs/cleanup-readme 2022-11-24 18:26:49 +00:00
Unlock Music Dev 8a95239c35
fix: remove broken test 2022-11-24 21:49:51 +08:00
Unlock Music Dev b811dd6bc5
fix: KgmWasm build target 2022-11-24 21:41:09 +08:00
Unlock Music Dev 4d3ef0ddaa Merge pull request 'Add Tag Edit Function & Wasm for Qmc & Kgm' (#5) from xhacker-zzz/web:master into master
Reviewed-on: um/web#5
2022-11-24 07:32:59 +00:00
Unlock Music Dev c8c4f8dfd6
fix: sync package-lock with package 2022-11-24 15:11:13 +08:00
Unlock Music Dev c25a2963fe
fix: ignore QmcWasm build dist 2022-11-24 15:11:13 +08:00
Unlock Music Dev 9bf05fc15a
fix: compile error 2022-11-24 15:11:12 +08:00
Unlock Music Dev e2fde927f6
fix: script permission 2022-11-24 15:11:08 +08:00
xhacker-zzz 3afa797cf9 Delete generated files 2022-11-24 04:59:07 +08:00
xhacker-zzz 6514befb3a Update qmccache cpp code 2022-11-24 04:54:48 +08:00
xhacker-zzz 4716579b20 Update annotation & log output 2022-11-24 04:43:16 +08:00
xhacker-zzz b63683781c Merge remote-tracking branch 'upstream/master' 2022-11-24 04:40:04 +08:00
Jixun Wu efa3692a54 其它: 移除主页说明失效的链接、更名 `.github` 到 `.gitlab`、加入 issue 模板。 2022-11-22 01:20:06 +00:00
xhacker-zzz 6f6de504d8 Fix Bugs 2022-11-22 06:59:35 +08:00
xhacker-zzz b5cc9a7404 Add mg3d & Fix Bugs 2022-11-22 06:40:02 +08:00
Unlock Music Dev 49e0526ae9 Merge pull request '优化 QMCCache 解密过程' (#6) from jixunmoe/um-web:refactor/optimise-qm-cache-decode into master
Reviewed-on: um/web#6
2022-11-21 19:48:05 +00:00
Jixun Wu 8b2834b06a refactor: implement a more efficient buffer decryption algorithm for qmc cache. 2022-11-21 18:15:16 +00:00
Jixun Wu 2dff0c1f37 test: added test cases for qmc cache. 2022-11-21 18:14:44 +00:00
xhacker-zzz 6d2865b870 Fix Bugs & Add More Detailed Notifications 2022-11-22 01:53:22 +08:00
xhacker-zzz 1138d1ea0c Delete unnecessary code 2022-11-21 05:14:45 +08:00
xhacker-zzz ddceb2ede8 Add README & Global Build Script for WASM 2022-11-21 04:55:43 +08:00
xhacker-zzz 63dc5c125c Add GitHub Actions for wasm & chores 2022-11-21 01:48:24 +08:00
xhacker-zzz 3a8492b651 添加WASM部分源代码 2022-11-21 01:07:18 +08:00
xhacker-zzz 5c21f459c6 修正代码中的细节问题 2022-11-21 00:33:03 +08:00
xhacker-zzz 5d1d56e587 Add Tag Edit Function & Wasm for Qmc & Kgm 2022-11-20 22:30:56 +08:00
Unlock Music Dev 27bccbbd55 Merge pull request '不依赖远端文件实现 KGM 解锁(内嵌 ~1K 的 mask 表数据)' (#2) from jixunmoe/um-web:master into master
Reviewed-on: um/web#2
2022-10-31 17:11:13 +00:00
Jixun 5db14d1d2a chore: bump dependency version. 2022-04-28 00:42:06 +01:00
Jixun 4872efbf87 feat: kgm unlock without large / remote mask file 2022-04-28 00:36:20 +01:00
邴新科 6c51c1a17d
暗黑样式:解密设定窗口 .el-dialog
(cherry picked from commit eb9f7bc003ecaa755240330b2ea32f743be270df)
2022-03-02 05:07:39 +08:00
邴新科 9688cf13c9
+ 新功能提示框(白/黑)
+ fix: 暗黑模式 一些小问题
+ 一些 scss 通用方法

(cherry picked from commit 8c149b6fa82dde3d2c03c236c699939154c65508)
2022-03-02 05:07:20 +08:00
MengYX 1f7ac44829
fix(QMCv2): `.mggl` `.mgg0`
(cherry picked from commit f7b5e25277678ae0f78a186aaf2fa76fee86173f)
2022-01-21 11:47:04 +08:00
MengYX 08a6f1d946 更新.gitlab-ci.yml文件 2022-01-09 11:30:01 +08:00
MengYX 05b47e3188 更新.gitlab-ci.yml文件 2022-01-09 11:29:26 +08:00
MengYX 9525045cd9 更新.gitlab-ci.yml文件 2022-01-09 11:23:42 +08:00
MengYX 4a5f3849a0 更新.gitlab-ci.yml文件 2022-01-09 11:17:52 +08:00
MengYX 711468601a
chore: update deps & bump version
(cherry picked from commit c48dbd8a350d0df22e997ccf9fbdc4e889ddd379)
2022-01-09 10:47:34 +08:00
MengYX 48255367b4
docs: update README.md
(cherry picked from commit 49c53a9c65dc8aa37a9b7413ab937854ecc1c20a)
2022-01-09 10:47:33 +08:00
MengYX 8260e744ca
refactor: change common -> index
(cherry picked from commit eb6be0d9d13a0834b6a7ada9aab846cfbbdfd664)
2022-01-09 10:46:57 +08:00
MengYX 976077e3e1
fix(QMCv2): overflow error in js decoder
(cherry picked from commit 191ac6a932efb290f49e2824839db20ac6ff47ca)
2022-01-09 10:46:55 +08:00
MengYX 4f7d16e2b0
fix #222
(cherry picked from commit 53127de96851ef24f774169ae5e55274f800bd99)
2022-01-09 10:46:54 +08:00
MengYX af10576b88
fix: declare radix in parseInt
Co-authored-by: Jixun Wu <5713045+jixunmoe@users.noreply.github.com>
(cherry picked from commit eee7d7aedc37797820184c12f6954df23e4b98b8)
2022-01-09 10:46:48 +08:00
Jixun Wu fc4a0e002f
fix(QMCv2): Fail gracefully when WebAssembly loader failed.
(cherry picked from commit 700574fb8efd1a1057bc16a053e5015dcb2e3044)
2022-01-09 10:46:46 +08:00
Jixun Wu 55772dec31
feat(QMCv2): Allow extraction of songId from QMC2-wasm
(cherry picked from commit 9ca2d852ce713255caeb8424a2724cb936434f18)
2022-01-09 10:46:45 +08:00
Jixun Wu a08e189d8e
chore: bump qmc2-crypto to v0.0.6-R1
(cherry picked from commit e29b36229e3f550a2fe8dd9d3ae48826f6229ec7)
2022-01-09 10:46:43 +08:00
MengYX 0b786b9885
feature: use online info to correct qmc meta
(cherry picked from commit 7c62f35adb268509f543d67b4a36f49ada3ae206)
2022-01-09 10:46:42 +08:00
MengYX b710255100
fix #142: remove default mp3 cover description
(cherry picked from commit 4bca64b4b8f4be02222de2fa5f6db0382855fd23)
2022-01-09 10:46:35 +08:00
MengYX 381f224a68
fix: api path & docker image name
(cherry picked from commit ce3de22d0e25f8cdf13e455baaae657bf61ee56a)
2022-01-09 10:46:21 +08:00
Jixun Wu a73212f026
test(joox): Added basic sanity test for joox encryption.
(cherry picked from commit 48b8194363264a0276006deaa3c956970a543627)
2022-01-09 10:46:19 +08:00
Jixun Wu c098b73617
feat(joox): Fetch meta data from API
(cherry picked from commit 4af1a38334cfc51ce64dd509f2dff694f78010f6)
2022-01-09 10:46:18 +08:00
Jixun Wu 107eaef5e6
doc: reforamtted & updated content in readme
(cherry picked from commit 68782f0ec570f7e4ec7ae5adc0bcd7da7a0d64b9)
2022-01-09 10:46:17 +08:00
Jixun Wu eb61d81817
fix: avoid "ArtiomTr/jest-coverage-report-action" when running from a fork.
(cherry picked from commit 95df64516c59f4bbbffa21625af8f9be13da01af)
2022-01-09 10:45:18 +08:00
Jixun Wu c1aba5a10f
refactor: move ruby to custom vue component
(cherry picked from commit ea99d38a920850b0d4dbaa7352f57ebf13bbbee6)
2022-01-09 10:45:17 +08:00
Jixun Wu 3567e7e625
chore: remove left-over debugger statement
(cherry picked from commit afab80505e343830e7b20a8073d21bdbfc7e3502)
2022-01-09 10:45:16 +08:00
Jixun Wu 302f422a5f
fix: form validation on input change
(cherry picked from commit c20ce54dacfe4ccc31974a9a9092938ed47db4bb)
2022-01-09 10:45:14 +08:00
Jixun Wu 1c4185750a
feat(config): better config ui
- JOOX Music UUID label + description
- Not full screen anymore

(cherry picked from commit 8a323f9dbbd17515f53826023a565112acaed90b)
2022-01-09 10:45:06 +08:00
Jixun Wu 13f2d86df4
feat(joox): re-use QM meta extraction code
(cherry picked from commit 2e946e6e30e02085018e868b7857acb62a1a0b08)
2022-01-09 10:45:04 +08:00
Jixun Wu 4ae92cd630
refactor(qmc): extract qm meta code to utils
(cherry picked from commit b6497e2bd3679e0e62bd6a90bdac16fa7c7f1b4e)
2022-01-09 10:45:02 +08:00
Jixun Wu 57d2244c28
fix: only pass over config settings
(cherry picked from commit 3884158f06b71907f004d7a2b4df53e3e486983b)
2022-01-09 10:45:01 +08:00
Jixun Wu 5e6cf8bf24
feat(storage): Pass over config to worker thread on decryption call
(cherry picked from commit 36d616398eac4e8d51863863fa5205fe1c91267f)
2022-01-09 10:44:59 +08:00
Jixun Wu 10aa05c244
refactor: storage factory + singleton
- Make storage easier.

(cherry picked from commit ed84a4732d7dd3ce6b2c22f30553ab5c59f85dbb)
2022-01-09 10:44:57 +08:00
Jixun 5d3d8ce485
fix: storage read/write in chrome extension
(cherry picked from commit bae9a7fec0c98807b3c5c3598f321135ccf6c9d5)
2022-01-09 10:44:56 +08:00
Jixun f94266bacc
fix: add missing permission for chrome storage
(cherry picked from commit 3fb0e1eb0f80cdb84fce6c2eb2a12a028beb1f0b)
2022-01-09 10:44:54 +08:00
Jixun f414f978b4
chore: bump to 0.0.1-R4
(cherry picked from commit 2da37f984a8ed4ca369e2efecb2da5d71976c93e)
2022-01-09 10:44:52 +08:00
Jixun ac6336993e
fix: crash due to chrome been undefined
(cherry picked from commit 53a2073cb482fc9deef7aa0ddf45447c6971d819)
2022-01-09 10:44:50 +08:00
Jixun 8a17dd352d
chore: bump joox-crypto dependency
(cherry picked from commit b46d9fa720a9193ae51b5954e2e34c875e1cc897)
2022-01-09 10:44:49 +08:00
Jixun dbfff5feca
feat: add basic joox support
(cherry picked from commit 699333ca06526d747a7eb4a188e896de81e9f014)
2022-01-09 10:44:36 +08:00
MengYX 3d86eb19b9
fix(ci): test coverage annotation failed
(cherry picked from commit 058985de4f003e2fbdfc4261e2d172c1f9c1c4db)
2022-01-09 10:44:32 +08:00
MengYX 6287283cde
pretty: ignore matrix
(cherry picked from commit edc4e4864b31c32e5860b3a5c840657be6cc4154)
2022-01-09 10:44:28 +08:00
MengYX 477d66e9e9
chore: remove unused api
(cherry picked from commit 3727f67e407807de33be64905b927561aaf1c10f)
2022-01-09 10:44:22 +08:00
MengYX 3441b7a3b1
all: format with prettier
(cherry picked from commit cad5b4d7deba4fbe4a40a17306ce49d3b2f13139)
2022-01-09 10:44:16 +08:00
MengYX 5dc89502cb
maintenance: add prettier
(cherry picked from commit 559be402c940b7b31bdb2567c23ff17251aabe04)
2022-01-09 10:38:33 +08:00
MengYX b9efb68851
fix(QMCv2): cipher should determine by key size
(cherry picked from commit dba63f212cbf9351e5dc16870eb32ae582db2867)
2022-01-09 10:38:27 +08:00
MengYX c1320c811b
chore(QMCv2): fix code style
(cherry picked from commit 87138718549bdec014752ba43dcd5997aaf29137)
2022-01-09 10:38:20 +08:00
MengYX fce4734ed9
feat(QMCv2): use js decoder
(cherry picked from commit c24e5d29733cfa771dd41ae40032029c6bbb9186)
2022-01-09 10:38:18 +08:00
MengYX 334864f6d8
feat(QMCv2): add decoder
(cherry picked from commit 29ac94d1fe52e666fda619f8716d2bc0b120a9ee)
2022-01-09 10:38:16 +08:00
MengYX 76c3887eec
feat(QMCv2): add rc4 cipher
(cherry picked from commit 6b5b4d3bf5f6285e908808d48dee4e2e4ae8c3a2)
2022-01-09 10:38:14 +08:00
MengYX 4703667a44
feat(QMCv2): add key decrypt
(cherry picked from commit a9aaa40ec48a75967882ef95951bf4f7fccf7a9d)
2022-01-09 10:38:13 +08:00
MengYX 183ac63864
feat(QMCv2): add map cipher
(cherry picked from commit 7306bf031f8bc07168197c00e332bf89c8d611dd)
2022-01-09 10:38:11 +08:00
MengYX d5ac9ad56e
test(QMCv2): coverage standard TEA cipher
(cherry picked from commit c2c89a423ffffc06fb43c86d4714bb32d1936c3e)
2022-01-09 10:38:09 +08:00
MengYX 7b3b701924
feat(QMCv2): add standard TEA cipher
(cherry picked from commit 24422b216a15319d90799d4f8f54453c8efd5c34)
2022-01-09 10:38:08 +08:00
MengYX 97ef3f0d7b
fix(extension): version string must be numbers and dots
(cherry picked from commit 3fd35b5d30037a6e156fdb75ca4124837b37d658)
2021-12-16 10:46:20 +08:00
MengYX cbed2332fb
fix: ci
(cherry picked from commit f9f5e32b449c9268cc07f7787587f417d70f08c9)
2021-12-16 09:51:28 +08:00
MengYX fab64f19d4
maintenance: update ci
(cherry picked from commit 525ddfae314f05e1d9a7b67cabcc974b32a503b4)
2021-12-16 09:51:23 +08:00
MengYX 70b46f9d63
maintenance: update ci
(cherry picked from commit 10e35c5d3e4391e22fd005f04ab7be0e503c971a)
2021-12-16 09:51:21 +08:00
MengYX de01d7ff9c
chore: bump version
(cherry picked from commit 12e3f91a1e9a4d681633d531af22b5e385dbe470)
2021-12-16 09:51:19 +08:00
MengYX 768f30a2fe
maintenance: update ci
(cherry picked from commit cb92eed9b135c04a17389f48997c18ba81e60c3a)
2021-12-16 09:51:17 +08:00
MengYX 59048aef6b
maintenance: update ci
(cherry picked from commit 3960ea7d591a199c188e764b26d0840ccae1c322)
2021-12-16 09:51:16 +08:00
MengYX 60ea6239eb
maintenance: remove fix-compatibility.js
(cherry picked from commit af20e8a6970ec7f08799389ac9ce897d1cc822e0)
2021-12-16 09:51:15 +08:00
MengYX 42ea3651f2
feat: use static cipher instead of mask
(cherry picked from commit cd6b84ad7eed489f9bcbd72d847cd4d704052b0c)
2021-12-16 09:51:07 +08:00
Jixun Wu 7f48acd214
chore: add eol at the end of `qmcv2.ts`.
(cherry picked from commit 9470f2ca8706d602c6d073012d4c3fc6aec7da77)
2021-12-16 09:50:20 +08:00
Jixun Wu 8e007ff0a3
chore: (redone) upgrade qmc2-crypto to 0.0.5-R4
- Remove the use of `new Function` in emscripten generated code.
- This commit is a clean commit that does the same thing as 3b88d168b660f780824016e4d23241d1fc766e39

(cherry picked from commit bdd60bc502ace1116698ff16357001bfb7608a43)
2021-12-16 09:49:56 +08:00
Jixun Wu b2bf878c89
Revert "chore: upgrade `qmc2-crypto` to 0.0.5-R4"
This reverts commit 3b88d168b660f780824016e4d23241d1fc766e39.

It generates unexpected large diff in package-lock.json.

(cherry picked from commit 0f3cd9b67fbc0f91da5272eb60301e09e4fc6de3)
2021-12-16 09:49:34 +08:00
Jixun Wu b550b407e5
chore: upgrade `qmc2-crypto` to 0.0.5-R4
- Remove the use of `new Function` in emscripten generated code.

(cherry picked from commit 3b88d168b660f780824016e4d23241d1fc766e39)
2021-12-16 09:49:10 +08:00
Jixun Wu 1e927ad962
fix: treat qmcflac/qmcogg as QMCv2 and fallback to QMCv1
(cherry picked from commit 41e588e9864801897fa13eb96a1764baaa5a4ab5)
2021-12-16 09:48:52 +08:00
Jixun Wu 9edcaadb83
refactor: remove suppressed qmc mask methods / constants
(cherry picked from commit 5d48b28a949cbd42f24781a69124d7aa521e51c1)
2021-12-16 09:48:31 +08:00
Jixun Wu ec6be66cc1
refactor: restore support for QMCv1.
(cherry picked from commit 19239f182d71e2e4362309f08706a91c00bb6bd1)
2021-12-16 09:48:30 +08:00
Jixun Wu ab3f54cb47
chore: update supported ext list
(cherry picked from commit bdab51bde327244a105fff5c2086911b275b2259)
2021-12-16 09:48:13 +08:00
Jixun Wu da11e3a9a1
chore: Use QMC2-Crypto with embedded WASM build from 0.0.5-R3
(cherry picked from commit 9448b497ed6b80e41f0e9f731f1ffa1e56fb149a)
2021-12-16 09:48:06 +08:00
Jixun Wu 8d35afae62
fix: patch threads to work with production build
(cherry picked from commit 4da56bb0fe509c4cb0c4bb6e560b4383f185bf45)
2021-12-16 09:47:41 +08:00
Jixun Wu 840d750716
feat(qmcv2): Experiment with qmc2-crypto
(cherry picked from commit c8eb1bc481347efb6d35e9122e17e624bde18772)
2021-12-16 09:47:40 +08:00
MengYX ada078df19
maintenance: add jest as unit test 2021-12-14 15:39:52 +08:00
MengYX 8facd65834
chore: update deps 2021-12-14 14:57:48 +08:00
MengYX a6367401ae
Merge pull request #184 from lvzx123/patch-1
Now it is 2021!
2021-09-25 15:27:57 +08:00
lvzx123 565ee37a8b
Now it is 2021!
大人,时代变了
2021-09-25 08:56:07 +08:00
MengYX aab611bff5
bump version 2021-08-27 10:01:05 +08:00
MengYX 942e43fa16
fix #169 2021-08-25 01:04:46 +08:00
MengYX 84be57a903
fix #179 2021-08-24 23:55:44 +08:00
MengYX a6a3c02538
fix #179 2021-08-24 23:13:19 +08:00
Emmm Monster 216ff4021c
fix(extension): compute version name 2021-08-11 23:04:01 +08:00
Emmm Monster d3898161b9
chore: bump version & update deps 2021-08-08 08:08:56 +08:00
MengYX 5a7a9e3add
change ixarea api endpoint 2021-08-08 07:56:28 +08:00
MengYX 652bb1fc32
optimize: imports 2021-08-08 07:51:42 +08:00
MengYX 6737e8c11b
optimize: `.kgm` mask loading 2021-08-08 07:47:28 +08:00
sunhao03 71862538b7 fetch mask file fix on production 2021-08-07 22:13:00 +08:00
Emmm Monster 4251b94b1f
chore: update deps & fix audit 2021-07-01 01:33:28 +08:00
Emmm Monster 8fdda048f6
fix: avoid using worker in file protocol 2021-07-01 01:29:04 +08:00
Emmm Monster 39c7294996
chore(ci): build after *.ts changes 2021-06-03 13:15:04 +08:00
Emmm Monster 48f879cb58
simplify: decrypt/ncm-cache & decrypt/common 2021-06-03 13:09:14 +08:00
Emmm Monster f0875ad175
fix: decrypt/qmc-cache
adapt: decrypt/qmc for qmc-cache
2021-06-03 13:09:02 +08:00
qq1010903229 02a146e069
增加对网易云音乐.uc缓存格式和QQ音乐.cache缓存格式的支持 (#161)
* Update common.ts

* Create ncmcache.ts

* Create qmccache.ts
2021-06-03 13:00:35 +08:00
Emmm Monster 2e31853ffb
chore: update deps 2021-05-27 19:53:19 +08:00
Emmm Monster a7aaf246ae
fix: .vpr/.kgm fail in worker 2021-05-27 19:38:42 +08:00
Emmm Monster 4bc0a10c09
feature(sniffer): support `.dff` 2021-05-25 12:27:19 +08:00
Emmm Monster 3645dd7d01
fix: remove test file 2021-05-25 04:36:32 +08:00
Emmm Monster 73bb9438b1
chore: Bump Version 2021-05-25 03:20:42 +08:00
Emmm Monster 21d5ae305c
feature: directly write to fs 2021-05-25 03:06:28 +08:00
Emmm Monster 759c1bd87e
fixes 2021-05-24 23:48:52 +08:00
Emmm Monster c7e5dfb4c4
refactor: component/*.vue 2021-05-24 22:19:37 +08:00
Emmm Monster ca4ed149b2
refactor(decrypt/*): change interface 2021-05-24 15:05:14 +08:00
Emmm Monster b3c6fe2f24
refactor(decrypt/qmc): typescript 2021-05-24 06:50:20 +08:00
Emmm Monster aca1c11332
refactor(decrypt/qmc): typescript qmc mask 2021-05-24 05:57:04 +08:00
Emmm Monster 15dba7b92f
refactor(.ncm): typescript & class 2021-05-24 05:04:16 +08:00
Emmm Monster 37a641e69e
refactor(typescript): utils.WriteMetaFor{ Mp3, Flac } 2021-05-24 02:55:42 +08:00
Emmm Monster 342241b379
refactor(typescript): .xm & .kgm 2021-05-24 01:30:38 +08:00
138 changed files with 33463 additions and 5940 deletions

30
.drone.yml Normal file
View File

@ -0,0 +1,30 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: build-wasm
image: emscripten/emsdk:3.0.0
commands:
- ./scripts/build-wasm.sh
- name: build
image: node:16.18-bullseye
commands:
- apt-get update
- apt-get install -y jq zip
- npm ci
- npm run test
- ./scripts/build-and-package.sh legacy
- ./scripts/build-and-package.sh extension
- ./scripts/build-and-package.sh modern
- name: upload artifact
image: node:16.18-bullseye
environment:
DRONE_GITEA_SERVER: https://git.unlock-music.dev
GITEA_API_KEY:
from_secret: GITEA_API_KEY
commands:
- ./scripts/upload-packages.sh

View File

@ -1,74 +0,0 @@
name: Test Build
on:
push:
paths:
- "**/*.js"
- "**/*.vue"
- "public/**/*"
- "package-lock.json"
- "package.json"
pull_request:
branches: [ master ]
types: [ opened, synchronize, reopened ]
paths:
- "**/*.js"
- "**/*.vue"
- "public/**/*"
- "package-lock.json"
- "package.json"
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
build: [ legacy, modern ]
include:
- build: legacy
BUILD_ARGS: ""
BUILD_EXTENSION: true
- build: modern
BUILD_ARGS: "-- --modern"
BUILD_EXTENSION: false
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Install Dependencies
run: |
npm ci
npm run fix-compatibility
- name: Build
env:
GZIP: "--best"
run: |
npm run build ${{ matrix.BUILD_ARGS }}
tar -czvf dist.tar.gz -C ./dist .
- name: Build Extension
if: ${{ matrix.BUILD_EXTENSION }}
run: |
npm run make-extension
cd dist
zip -rJ9 ../extension.zip *
cd ..
- name: Publish artifact
uses: actions/upload-artifact@v2
with:
name: unlock-music-${{ matrix.build }}.tar.gz
path: ./dist.tar.gz
- name: Publish artifact - Extension
if: ${{ matrix.BUILD_EXTENSION }}
uses: actions/upload-artifact@v2
with:
name: extension.zip
path: ./extension.zip

View File

@ -1,65 +0,0 @@
name: Post Release
on:
release:
types: [ published ]
jobs:
release-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup vars
id: vars
env:
RELEASE_REF: ${{ github.ref }}
run: echo "::set-output name=tag::${RELEASE_REF#refs/tags/}"
- name: Download release content
run: |
echo "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
wget -O modern.tar.gz "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
mkdir ./dist
tar zxf modern.tar.gz -C ./dist
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build docker and push (on modern)
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/386
push: true
tags: |
ix64/unlock-music:latest
ix64/unlock-music:${{ steps.vars.outputs.tag }}
gh-pages:
runs-on: ubuntu-latest
steps:
- name: Setup vars
id: vars
env:
RELEASE_REF: ${{ github.ref }}
run: echo "::set-output name=tag::${RELEASE_REF#refs/tags/}"
- name: Download release content
run: |
echo "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
wget -O modern.tar.gz "https://github.com/${{ github.repository }}/releases/download/${{ steps.vars.outputs.tag }}/modern.tar.gz"
mkdir ./dist
tar zxf modern.tar.gz -C ./dist
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist

View File

@ -1,128 +0,0 @@
name: Build Release
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Install Dependencies
run: |
npm ci
npm run fix-compatibility
- name: Build Legacy
env:
GZIP: "--best"
run: |
npm run build
tar -czf legacy.tar.gz -C ./dist .
cd dist
zip -rJ9 ../legacy.zip *
cd ..
- name: Build Extension (on legacy)
env:
GZIP: "--best"
run: |
npm run make-extension
cd dist
zip -rJ9 ../extension.zip *
cd ..
- name: Build Modern
env:
GZIP: "--best"
run: |
npm run build -- --modern
tar -czf modern.tar.gz -C ./dist .
cd dist
zip -rJ9 ../modern.zip *
cd ..
- name: Checksum
run: sha256sum *.tar.gz *.zip > sha256sum.txt
- name: Get current time
id: date
run: echo "::set-output name=date::$(date +'%Y/%m/%d')"
- name: Create a Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: "Build ${{ steps.date.outputs.date }}"
draft: true
- name: Upload Release Assets - legacy.tar.gz
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./legacy.tar.gz
asset_name: legacy.tar.gz
asset_content_type: application/gzip
- name: Upload Release Assets - legacy.zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./legacy.zip
asset_name: legacy.zip
asset_content_type: application/zip
- name: Upload Release Assets - modern.tar.gz
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./modern.tar.gz
asset_name: modern.tar.gz
asset_content_type: application/gzip
- name: Upload Release Assets - modern.zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./modern.zip
asset_name: modern.zip
asset_content_type: application/zip
- name: Upload Release Assets - extension.zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./extension.zip
asset_name: extension.zip
asset_content_type: application/zip
- name: Upload Release Assets - sha256sum.txt
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./sha256sum.txt
asset_name: sha256sum.txt
asset_content_type: text/plain

13
.gitignore vendored
View File

@ -1,6 +1,8 @@
.DS_Store
node_modules
/dist
/build
/coverage
# local env files
.env.local
@ -19,3 +21,14 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?
/src/KgmWasm/build
/src/KgmWasm/*.js
/src/KgmWasm/*.wasm
/src/QmcWasm/build
/src/QmcWasm/*.js
/src/QmcWasm/*.wasm
*.zip
*.tar.gz
/sha256sum.txt

47
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,47 @@
image: node:16
cache:
paths:
- node_modules/
stages:
- build
build-job:
stage: build
script: |
sed -i 's/deb.debian.org/mirrors.cloud.tencent.com/g' /etc/apt/sources.list
apt-get update
apt-get -y install zip
npm config set registry http://mirrors.cloud.tencent.com/npm/
npm ci
npm run build
tar -czf legacy.tar.gz -C ./dist .
cd dist
zip -rJ9 ../legacy.zip *
cd ..
npm run make-extension
cd dist
zip -rJ9 ../extension.zip *
cd ..
npm run build -- --modern
tar -czf modern.tar.gz -C ./dist .
cd dist
zip -rJ9 ../modern.zip *
cd ..
sha256sum *.tar.gz *.zip > sha256sum.txt
artifacts:
name: "$CI_JOB_NAME"
paths:
- legacy.zip
- legacy.tar.gz
- extension.zip
- modern.zip
- modern.tar.gz
- sha256sum.txt

View File

@ -1,39 +1,39 @@
---
name: Bug报告
about: 报告Bug以帮助改进程序
title: ''
labels: bug
assignees: ''
---
* 请按照此模板填写,否则可能立即被关闭
- [x] 我确认已经搜索过Issue不存并确认相同的Issue
- [x] 我有证据表明这是程序导致的问题(如不确认,可以在[Discussions](https://github.com/ix64/unlock-music/discussions)内提出)
**Bug描述**
简要地复述你遇到的Bug
**复现方法**
描述复现方法,必要时请提供样本文件
**程序截图或者Console报错信息**
如果可以请提供二者之一
**环境信息:**
- 操作系统和浏览器:
- 程序版本:
- 获取音乐文件所使用的客户端及其版本信息:
**附加信息**
其他能够帮助确认问题的信息
---
name: Bug报告
about: 报告Bug以帮助改进程序
title: ''
labels: bug
assignees: ''
---
* 请按照此模板填写,否则可能立即被关闭
- [x] 我确认已经搜索过Issue不存并确认相同的Issue
- [x] 我有证据表明这是程序导致的问题(如不确认,可以在[Discussions](https://github.com/ix64/unlock-music/discussions)内提出)
**Bug描述**
简要地复述你遇到的Bug
**复现方法**
描述复现方法,必要时请提供样本文件
**程序截图或者Console报错信息**
如果可以请提供二者之一
**环境信息:**
- 操作系统和浏览器:
- 程序版本:
- 获取音乐文件所使用的客户端及其版本信息:
**附加信息**
其他能够帮助确认问题的信息

View File

@ -1,26 +1,26 @@
---
name: 新功能
about: 对于程序新的想法或建议
title: ''
labels: enhancement
assignees: ''
---
- 请按照此模板填写,否则可能立即被关闭
**背景和说明**
简要说明产生此想法的背景和此想法的具体内容
**实现途径**
- 如果没有设计方案,请简要描述实现思路
- 如果你没有任何的实现思路,请通过[Discussions](https://github.com/ix64/unlock-music/discussions)或者Telegram进行讨论
**附加信息**
更多你想要表达的内容
---
name: 新功能
about: 对于程序新的想法或建议
title: ''
labels: enhancement
assignees: ''
---
- 请按照此模板填写,否则可能立即被关闭
**背景和说明**
简要说明产生此想法的背景和此想法的具体内容
**实现途径**
- 如果没有设计方案,请简要描述实现思路
- 如果你没有任何的实现思路,请通过[Discussions](https://github.com/ix64/unlock-music/discussions)或者Telegram进行讨论
**附加信息**
更多你想要表达的内容

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v16.18.1

42
.prettierrc.js Normal file
View File

@ -0,0 +1,42 @@
// .prettierrc.js
module.exports = {
// 一行最多 120 字符
printWidth: 120,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾需要有逗号
trailingComma: 'all',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
bracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// vue 文件中的 script 和 style 内不用缩进
vueIndentScriptAndStyle: false,
// 换行符使用 lf
endOfLine: 'lf',
// 格式化嵌入的内容
embeddedLanguageFormatting: 'auto',
};

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019-2020 MengYX
Copyright (c) 2019-2023 MengYX
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

103
README.md
View File

@ -1,71 +1,90 @@
# Unlock Music 音乐解锁
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- unlock-music项目是以学习和技术研究的初衷创建的修改、再分发时请遵循[License](https://github.com/ix64/unlock-music/blob/master/LICENSE)
- Unlock Music的CLI版本正在开发中。
- 我们新建了Telegram群组欢迎加入[https://t.me/unlock_music_chat](https://t.me/unlock_music_chat)
- [CLI版本 Alpha](https://github.com/unlock-music/cli) 大批量转换建议使用CLI版本
- [相关的其他项目](https://github.com/ix64/unlock-music/wiki/%E5%92%8CUnlockMusic%E7%9B%B8%E5%85%B3%E7%9A%84%E9%A1%B9%E7%9B%AE)
[![Build Status](https://ci.unlock-music.dev/api/badges/um/web/status.svg)](https://ci.unlock-music.dev/um/web)
![Test Build](https://github.com/ix64/unlock-music/workflows/Test%20Build/badge.svg)
![GitHub releases](https://img.shields.io/github/downloads/ix64/unlock-music/total)
![Docker Pulls](https://img.shields.io/docker/pulls/ix64/unlock-music)
- 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser.
- Unlock Music 项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循[授权协议]。
- Unlock Music 的 CLI 版本可以在 [unlock-music/cli] 找到,大批量转换建议使用 CLI 版本。
- 我们新建了 Telegram 群组 [`@unlock_music_chat`] ,欢迎加入!
- CI 自动构建已经部署,可以在 [UM-Packages] 下载
[授权协议]: https://git.unlock-music.dev/um/web/src/branch/master/LICENSE
[unlock-music/cli]: https://git.unlock-music.dev/um/cli
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
[UM-Packages]: https://git.unlock-music.dev/um/-/packages/generic/web-build/
## 特性
### 支持的格式
- [x] QQ音乐 (.qmc0/.qmc2/.qmc3/.qmcflac/.qmcogg/[.tkm](https://github.com/ix64/unlock-music/issues/9))
- [x] 写入封面图片
- [x] Moo音乐格式 ([.bkcmp3/.bkcflac](https://github.com/ix64/unlock-music/issues/11))
- [x] QQ音乐Tm格式 (.tm0/.tm2/.tm3/.tm6)
- [x] QQ音乐新格式 (实验性支持)
- [x] .mflac
- [x] [.mgg](https://github.com/ix64/unlock-music/issues/3)
- [x] QQ 音乐 (.qmc0/.qmc2/.qmc3/.qmcflac/.qmcogg/.tkm)
- [x] Moo 音乐格式 (.bkcmp3/.bkcflac/...)
- [x] QQ 音乐 Tm 格式 (.tm0/.tm2/.tm3/.tm6)
- [x] QQ 音乐新格式 (.mflac/.mgg/.mflac0/.mgg1/.mggl)
- [x] <ruby>QQ 音乐海外版<rt>JOOX Music</rt></ruby> (.ofl_en)
- [x] 网易云音乐格式 (.ncm)
- [x] 补全ncm的ID3/FlacMeta信息
- [x] 虾米音乐格式 (.xm) (测试阶段)
- [x] 酷我音乐格式 (.kwm) (测试阶段)
- [x] 酷狗音乐格式 (
.kgm) ([CLI版本](https://github.com/ix64/unlock-music/wiki/%E5%85%B6%E4%BB%96%E9%9F%B3%E4%B9%90%E6%A0%BC%E5%BC%8F%E5%B7%A5%E5%85%B7#%E9%85%B7%E7%8B%97%E9%9F%B3%E4%B9%90-kgmvpr%E8%A7%A3%E9%94%81%E5%B7%A5%E5%85%B7))
- [x] 虾米音乐格式 (.xm)
- [x] 酷我音乐格式 (.kwm)
- [x] 酷狗音乐格式 (.kgm/.vpr)
- [x] Android版喜马拉雅文件格式 (.x2m/.x3m)
- [x] 咪咕音乐格式 (.mg3d)
### 其他特性
- [x] 在浏览器中解锁
- [x] 拖放文件
- [x] 在线播放
- [x] 批量解锁
- [x] 渐进式Web应用
- [x] 渐进式 Web 应用 (PWA)
- [x] 多线程
- [x] 写入和编辑元信息与专辑封面
## 使用方法
### 安装浏览器扩展
### 使用预构建版本
[![Chrome Web Store](https://storage.googleapis.com/chrome-gcs-uploader.appspot.com/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/gldlhhhmienbhlpkfanjpmffdjblmegd)
[<img src="https://developer.microsoft.com/en-us/store/badges/images/Chinese_Simplified_get-it-from-MS.png" height="60" alt="Microsoft Edge Addons"/>](https://microsoftedge.microsoft.com/addons/detail/ggafoipegcmodfhakdkalpdpcdkiljmd)
[![Firefox Browser Addons](https://ffp4g1ylyit3jdyti1hqcvtb-wpengine.netdna-ssl.com/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/zh-CN/firefox/addon/unlock-music/)
### 使用已构建版本
- 从[GitHub Release](https://github.com/ix64/unlock-music/releases/latest)下载已构建的版本
- 本地使用请下载`legacy版本``modern版本`只能通过**http/https协议**访问)
- 从 [Release] 或 [CI 构建][UM-Packages] 下载预构建的版本
- :warning: 本地使用请下载`legacy版本``modern版本`只能通过 **http(s)协议** 访问)
- 解压缩后即可部署或本地使用(**请勿直接运行源代码**
### 使用Docker镜像
```shell
docker run --name unlock-music -d -p 8080:80 ix64/unlock-music
```
[release]: https://git.unlock-music.dev/um/web/releases/latest
### 自行构建
#### JS部分
- 环境要求
- nodejs
- nodejs (v16.x)
- npm
1. 获取项目源代码后执行 `npm install` 安装相关依赖
2. 执行 `npm run build` 即可进行构建,构建输出为 dist 目录
1. 获取项目源代码后安装相关依赖:
- `npm run serve` 可用于开发
3. 如需构建浏览器扩展build完成后还需要执行`npm run make-extension`
```sh
npm ci
```
2. 然后进行构建:
```sh
npm run build
```
- 构建后的产物可以在 `dist` 目录找到。
- 如果是用于开发,可以执行 `npm run serve`
3. 如需构建浏览器扩展,构建成功后还需要执行:
```sh
npm run make-extension
```
#### WASM部分
- 环境要求
- Linux
- python3
- 运行此目录下的build-wasm
```sh
./scripts/build-wasm.sh
```

View File

@ -1,6 +1,7 @@
module.exports = {
presets: [
'@vue/app'
'@vue/app',
'@babel/preset-typescript'
],
plugins: [
["component", {

View File

@ -6,6 +6,7 @@
"128": "./img/icons/msapplication-icon-144x144.png"
},
"description": "在任何设备上解锁已购的加密音乐!",
"permissions": ["storage"],
"offline_enabled": true,
"options_page": "./index.html",
"homepage_url": "https://github.com/ix64/unlock-music",

7
jest.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
testPathIgnorePatterns: ['/build/', '/dist/', '/node_modules/'],
setupFilesAfterEnv: ['./src/__test__/setup_jest.js'],
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
},
};

View File

@ -1,20 +1,25 @@
const fs = require('fs')
const path = require('path')
const src = "./src/extension/"
const dst = "./dist"
fs.readdirSync(src).forEach(file => {
let srcPath = path.join(src, file)
let dstPath = path.join(dst, file)
fs.copyFileSync(srcPath, dstPath)
console.log(`Copy: ${srcPath} => ${dstPath}`)
})
const manifestRaw = fs.readFileSync("./extension-manifest.json", "utf-8")
const manifest = JSON.parse(manifestRaw)
const pkgRaw = fs.readFileSync("./package.json", "utf-8")
const pkg = JSON.parse(pkgRaw)
manifest["version"] = pkg["version"]
fs.writeFileSync("./dist/manifest.json", JSON.stringify(manifest), "utf-8")
console.log("Write: manifest.json")
const fs = require('fs')
const path = require('path')
const src = __dirname + "/src/extension/"
const dst = __dirname + "/dist"
fs.readdirSync(src).forEach(file => {
let srcPath = path.join(src, file)
let dstPath = path.join(dst, file)
fs.copyFileSync(srcPath, dstPath)
console.log(`Copy: ${srcPath} => ${dstPath}`)
})
const manifestRaw = fs.readFileSync(__dirname + "/extension-manifest.json", "utf-8")
const manifest = JSON.parse(manifestRaw)
const pkgRaw = fs.readFileSync(__dirname + "/package.json", "utf-8")
const pkg = JSON.parse(pkgRaw)
verExt = pkg["version"]
if (verExt.startsWith("v")) verExt = verExt.slice(1)
if (verExt.includes("-")) verExt = verExt.split("-")[0]
manifest["version"] = `${verExt}.${pkg["ext_build"]}`
manifest["version_name"] = pkg["version"]
fs.writeFileSync(__dirname + "/dist/manifest.json", JSON.stringify(manifest), "utf-8")
console.log("Write: manifest.json")

31279
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
{
"name": "unlock-music",
"version": "1.8.1",
"updateInfo": "添加构建为Docker镜像",
"version": "1.10.3",
"ext_build": 0,
"updateInfo": "完善音乐标签编辑功能,支持编辑更多标签",
"license": "MIT",
"description": "Unlock encrypted music file in browser.",
"repository": {
@ -10,38 +11,49 @@
},
"private": true,
"scripts": {
"postinstall": "patch-package",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"fix-compatibility": "node ./src/fix-compatibility.js",
"test": "jest",
"pretty": "prettier --write src/{**/*,*}.{js,ts,jsx,tsx,vue}",
"pretty:check": "prettier --check src/{**/*,*}.{js,ts,jsx,tsx,vue}",
"make-extension": "node ./make-extension.js"
},
"dependencies": {
"@babel/preset-typescript": "^7.16.5",
"@jixun/kugou-crypto": "^1.0.3",
"@unlock-music/joox-crypto": "^0.0.1-R5",
"base64-js": "^1.5.1",
"browser-id3-writer": "^4.4.0",
"core-js": "^3.12.1",
"crypto-js": "^4.0.0",
"element-ui": "^2.15.1",
"iconv-lite": "^0.6.2",
"core-js": "^3.16.0",
"crypto-js": "^4.1.1",
"element-ui": "^2.15.5",
"iconv-lite": "^0.6.3",
"jimp": "^0.16.1",
"metaflac-js": "^1.0.5",
"music-metadata-browser": "^2.2.6",
"music-metadata": "7.9.0",
"music-metadata-browser": "2.2.7",
"register-service-worker": "^1.7.2",
"vue": "^2.6.12",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^9.1.2"
"threads": "^1.6.5",
"vue": "^2.6.14"
},
"devDependencies": {
"@types/crypto-js": "^4.0.2",
"@types/jest": "^27.0.3",
"@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-pwa": "^4.5.13",
"@vue/cli-plugin-typescript": "^4.5.13",
"@vue/cli-service": "^4.5.13",
"babel-plugin-component": "^1.1.1",
"node-sass": "^5.0.0",
"jest": "^27.4.5",
"patch-package": "^6.4.7",
"prettier": "2.5.1",
"sass": "^1.38.1",
"sass-loader": "^10.2.0",
"semver": "^7.3.5",
"typescript": "~4.1.5",
"threads-plugin": "^1.4.0",
"typescript": "^4.5.4",
"vue-cli-plugin-element": "^1.0.1",
"vue-template-compiler": "^2.6.12",
"workerize-loader": "^1.3.0"
"vue-template-compiler": "^2.6.14"
}
}
}

View File

@ -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

View File

@ -8,7 +8,6 @@
<title>音乐解锁</title>
<meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords"/>
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description"/>
<script src="./ixarea-stats.js"></script>
<!--@formatter:off-->
<style>#loader{position:absolute;left:50%;top:50%;z-index:1010;margin:-75px 0 0 -75px;border:16px solid #f3f3f3;border-radius:50%;border-top:16px solid #1db1ff;width:120px;height:120px;animation:spin 2s linear infinite}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}#loader-mask{text-align:center;position:absolute;width:100%;height:100%;bottom:0;left:0;right:0;top:0;z-index:1009;background-color:rgba(242,246,252,.88)}@media (prefers-color-scheme:dark){#loader-mask{color:#fff;background-color:rgba(0,0,0,.85)}#loader-mask a{color:#ddd}#loader-mask a:hover{color:#1db1ff}}#loader-source{font-size:1.5rem}#loader-tips-timeout{font-size:1.2rem}</style>
<!--@formatter:on-->
@ -19,9 +18,6 @@
<div id="loader"></div>
<noscript>
<h3 id="loader-js">请启用JavaScript</h3>
<img alt=""
src="https://stats.ixarea.com/ixarea-stats/report?rec=1&action_name=音乐解锁-NoJS&idsite=2"
style="border:0"/>
</noscript>
<h3 id="loader-source"> 请勿直接运行源代码! </h3>
<div id="loader-tips-outdated" hidden>

View File

@ -1,10 +0,0 @@
var _paq = window._paq || [];
_paq.push(["setRequestMethod", "POST"], ["trackPageView"], ["enableLinkTracking"],
["setSiteId", "2"], ["setTrackerUrl", "https://stats.ixarea.com/ixarea-stats/report"]);
var tag = document.createElement('script');
tag.type = 'text/javascript';
tag.async = true;
tag.src = 'https://stats.ixarea.com/ixarea-stats.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(tag, s);

Binary file not shown.

29
scripts/build-and-package.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/sh
set -ex
cd "$(git rev-parse --show-toplevel)"
VERSION="$(jq -r ".version" <package.json)"
DIST_NAME="um-web.$1.v${VERSION}"
case "$1" in
"modern") npm run build -- --modern ;;
"legacy") npm run build ;;
"extension") npm run make-extension ;;
*)
echo "Unknown command: $1"
exit 1
;;
esac
mv dist "${DIST_NAME}"
zip -rJ9 "${DIST_NAME}.zip" "${DIST_NAME}"
if [ "$1" = "legacy" ]; then
# For upcoming extension build
mv "${DIST_NAME}" dist
else
rm -rf "${DIST_NAME}"
fi

13
scripts/build-wasm.sh Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -ex
cd "$(git rev-parse --show-toplevel)"
pushd ./src/QmcWasm
bash build-wasm
popd
pushd ./src/KgmWasm
bash build-wasm
popd

19
scripts/upload-packages.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/sh
set -ex
cd "$(git rev-parse --show-toplevel)"
if [ -z "$GITEA_API_KEY" ]; then
echo "GITEA_API_KEY is empty, skip upload."
exit 0
fi
URL_BASE="$DRONE_GITEA_SERVER/api/packages/${DRONE_REPO_NAMESPACE}/generic/${DRONE_REPO_NAME}-build"
for ZIP_NAME in *.zip; do
UPLOAD_URL="${URL_BASE}/${DRONE_BUILD_NUMBER}/${ZIP_NAME}"
sha256sum "${ZIP_NAME}"
curl -sLifu "um-release-bot:$GITEA_API_KEY" -T "${ZIP_NAME}" "${UPLOAD_URL}"
echo "Uploaded to: ${UPLOAD_URL}"
done

View File

@ -1,184 +1,94 @@
<template>
<el-container id="app">
<el-main>
<x-upload v-on:handle_error="showFail" v-on:handle_finish="showSuccess"></x-upload>
<div id="app-control">
<el-row class="mb-3">
<span>歌曲命名格式</span>
<el-radio label="1" name="format" v-model="download_format">歌手-歌曲名</el-radio>
<el-radio label="2" name="format" v-model="download_format">歌曲名</el-radio>
<el-radio label="3" name="format" v-model="download_format">歌曲名-歌手</el-radio>
<el-radio label="4" name="format" v-model="download_format">同原文件名</el-radio>
</el-row>
<el-row>
<el-button @click="handleDownloadAll" icon="el-icon-download" plain>下载全部</el-button>
<el-button @click="handleDeleteAll" icon="el-icon-delete" plain type="danger">清除全部</el-button>
<el-tooltip class="item" effect="dark" placement="top-start">
<div slot="content">
当您使用此工具进行大量文件解锁的时候建议开启此选项<br/>
开启后解锁结果将不会存留于浏览器中防止内存不足
</div>
<el-checkbox border class="ml-2" v-model="instant_download">立即保存</el-checkbox>
</el-tooltip>
</el-row>
</div>
<audio :autoplay="playing_auto" :src="playing_url" controls/>
<x-preview :download_format="download_format" :table-data="tableData"
v-on:music_changed="changePlaying"></x-preview>
</el-main>
<el-footer id="app-footer">
<el-row>
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>(v<span
v-text="version"></span>)移除已购音乐的加密保护
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
</el-row>
<el-row>
目前支持网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
<a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>
</el-row>
<el-row>
<!--如果进行二次开发此行版权信息不得移除且应明显地标注于页面上-->
<span>Copyright &copy; 2019-</span><span v-text="(new Date()).getFullYear()"></span> MengYX
音乐解锁使用
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
开放源代码
</el-row>
</el-footer>
</el-container>
<el-container id="app">
<el-main>
<Home />
</el-main>
<el-footer id="app-footer">
<el-row>
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{ version }})
移除已购音乐的加密保护
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
</el-row>
<el-row>
目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
<a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>
</el-row>
<el-row>
<!--如果进行二次开发此行版权信息不得移除且应明显地标注于页面上-->
<span>Copyright &copy; 2019 - {{ new Date().getFullYear() }} MengYX</span>
音乐解锁使用
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
开放源代码
</el-row>
</el-footer>
</el-container>
</template>
<script>
import FileSelector from '@/component/FileSelector';
import PreviewTable from '@/component/PreviewTable';
import config from '@/../package.json';
import Home from '@/view/Home';
import { checkUpdate } from '@/utils/api';
import upload from "./component/upload"
import preview from "./component/preview"
import {DownloadBlobMusic, RemoveBlobMusic} from "./component/util"
import config from "../package"
import {IXAREA_API_ENDPOINT} from "./decrypt/util";
export default {
name: 'app',
components: {
xUpload: upload,
xPreview: preview
},
data() {
return {
version: config.version,
activeIndex: '1',
tableData: [],
playing_url: "",
playing_auto: false,
download_format: '1',
instant_download: false,
}
},
created() {
this.$nextTick(function () {
this.finishLoad();
});
},
methods: {
async finishLoad() {
const mask = document.getElementById("loader-mask");
if (!!mask) mask.remove();
let updateInfo;
try {
const resp = await fetch(IXAREA_API_ENDPOINT + "/music/app-version", {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({"Version": this.version})
});
updateInfo = await resp.json();
} catch (e) {
}
if ((!!updateInfo && process.env.NODE_ENV === 'production') && (!!updateInfo.HttpsFound ||
(!!updateInfo.Found && window.location.protocol !== "https:"))) {
this.$notify.warning({
title: '发现更新',
message: '发现新版本 v' + updateInfo.Version +
'<br/>更新详情:' + updateInfo.Detail +
'<br/><a target="_blank" href="' + updateInfo.URL + '">获取更新</a>',
dangerouslyUseHTMLString: true,
duration: 15000,
position: 'top-left'
});
} else {
this.$notify.info({
title: '离线使用',
message: '我们使用PWA技术无网络也能使用' +
'<br/>最近更新:' + config.updateInfo +
'<br/><a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
dangerouslyUseHTMLString: true,
duration: 10000,
position: 'top-left'
});
}
},
showSuccess(data) {
if (data.status) {
if (this.instant_download) {
DownloadBlobMusic(data, this.download_format);
RemoveBlobMusic(data);
} else {
this.tableData.push(data);
this.$notify.success({
title: '解锁成功',
message: '成功解锁 ' + data.title,
duration: 3000
});
}
if (process.env.NODE_ENV === 'production') {
let _rp_data = [data.title, data.artist, data.album];
window._paq.push(["trackEvent", "Unlock", data.rawExt + "," + data.mime, JSON.stringify(_rp_data)]);
}
} else {
this.showFail(data.message, data.rawFilename + "." + data.rawExt)
}
},
showFail(errInfo, filename) {
this.$notify.error({
title: '出现问题',
message: errInfo + "" + filename +
',参考<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
dangerouslyUseHTMLString: true,
duration: 6000
});
if (process.env.NODE_ENV === 'production') {
window._paq.push(["trackEvent", "Error", errInfo, filename]);
}
console.error(errInfo, filename);
},
changePlaying(url) {
this.playing_url = url;
this.playing_auto = true;
},
handleDeleteAll() {
this.tableData.forEach(value => {
RemoveBlobMusic(value);
});
this.tableData = [];
},
handleDownloadAll() {
let index = 0;
let c = setInterval(() => {
if (index < this.tableData.length) {
DownloadBlobMusic(this.tableData[index], this.download_format);
index++;
} else {
clearInterval(c);
}
}, 300);
}
},
}
export default {
name: 'app',
components: {
FileSelector,
PreviewTable,
Home,
},
data() {
return {
version: config.version,
};
},
created() {
this.$nextTick(() => this.finishLoad());
},
methods: {
async finishLoad() {
const mask = document.getElementById('loader-mask');
if (!!mask) mask.remove();
let updateInfo;
try {
updateInfo = await checkUpdate(this.version);
} catch (e) {
console.warn('check version info failed', e);
}
if (
updateInfo &&
process.env.NODE_ENV === 'production' &&
(updateInfo.HttpsFound || (updateInfo.Found && window.location.protocol !== 'https:'))
) {
this.$notify.warning({
title: '发现更新',
message: `发现新版本 v${updateInfo.Version}<br/>更新详情:${updateInfo.Detail}<br/> <a target="_blank" href="${updateInfo.URL}">获取更新</a>`,
dangerouslyUseHTMLString: true,
duration: 15000,
position: 'top-left',
});
} else {
this.$notify.info({
title: '离线使用',
message: `<div>
<p>我们使用 PWA 技术无网络也能使用</p>
<div class="update-info">
<div class="update-title">最近更新</div>
<div class="update-content"> ${config.updateInfo} </div>
</div>
<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>
</div>`,
dangerouslyUseHTMLString: true,
duration: 10000,
position: 'top-left',
});
}
},
},
};
</script>
<style lang="scss">
@import "scss/unlock-music";
@import 'scss/unlock-music';
</style>

View File

@ -0,0 +1,65 @@
# CMakeList.txt : CMake project for KgmWasm, include source and define
# project specific logic here.
#
cmake_minimum_required (VERSION 3.8)
project ("KgmWasm")
set(CMAKE_CXX_STANDARD 14)
include_directories(
$<TARGET_PROPERTY:INTERFACE_INCLUDE_DIRECTORIES>
)
# Add source to this project's executable.
set(RUNTIME_METHODS_LIST
getValue
writeArrayToMemory
UTF8ToString
)
list(JOIN RUNTIME_METHODS_LIST "," RUNTIME_METHODS)
set(EMSCRIPTEN_FLAGS
"--bind"
"-s NO_DYNAMIC_EXECUTION=1"
"-s MODULARIZE=1"
"-s EXPORT_NAME=KgmCryptoModule"
"-s EXPORTED_RUNTIME_METHODS=${RUNTIME_METHODS}"
)
set(EMSCRIPTEN_LEGACY_FLAGS
${EMSCRIPTEN_FLAGS}
"-s WASM=0"
"--memory-init-file 0"
)
set(EMSCRIPTEN_WASM_BUNDLE_FLAGS
${EMSCRIPTEN_FLAGS}
"-s SINGLE_FILE=1"
)
list(JOIN EMSCRIPTEN_FLAGS " " EMSCRIPTEN_FLAGS_STR)
list(JOIN EMSCRIPTEN_LEGACY_FLAGS " " EMSCRIPTEN_LEGACY_FLAGS_STR)
list(JOIN EMSCRIPTEN_WASM_BUNDLE_FLAGS " " EMSCRIPTEN_WASM_BUNDLE_FLAGS_STR)
# Define projects config
set(WASM_SOURCES
"KgmWasm.cpp"
)
add_executable(KgmWasm ${WASM_SOURCES})
set_target_properties(
KgmWasm
PROPERTIES LINK_FLAGS ${EMSCRIPTEN_FLAGS_STR}
)
add_executable(KgmWasmBundle ${WASM_SOURCES})
set_target_properties(
KgmWasmBundle
PROPERTIES LINK_FLAGS ${EMSCRIPTEN_WASM_BUNDLE_FLAGS_STR}
)
add_executable(KgmLegacy ${WASM_SOURCES})
set_target_properties(
KgmLegacy
PROPERTIES LINK_FLAGS ${EMSCRIPTEN_LEGACY_FLAGS_STR}
)

20
src/KgmWasm/KgmWasm.cpp Normal file
View File

@ -0,0 +1,20 @@
// KgmWasm.cpp : Defines the entry point for the application.
//
#include "KgmWasm.h"
#include "kgm.hpp"
#include <stddef.h>
#include <string.h>
size_t preDec(uintptr_t blob, size_t blobSize, std::string ext)
{
return PreDec((uint8_t*)blob, blobSize, ext == "vpr");
}
void decBlob(uintptr_t blob, size_t blobSize, size_t offset)
{
Decrypt((uint8_t*)blob, blobSize, offset);
return;
}

18
src/KgmWasm/KgmWasm.h Normal file
View File

@ -0,0 +1,18 @@
// KgmWasm.h : Include file for standard system include files,
// or project specific include files.
#pragma once
#include <emscripten/bind.h>
#include <string>
namespace em = emscripten;
size_t preDec(uintptr_t blob, size_t blobSize, std::string ext);
void decBlob(uintptr_t blob, size_t blobSize, size_t offset);
EMSCRIPTEN_BINDINGS(QmcCrypto)
{
em::function("preDec", &preDec, em::allow_raw_pointers());
em::function("decBlob", &decBlob, em::allow_raw_pointers());
}

9
src/KgmWasm/README.md Normal file
View File

@ -0,0 +1,9 @@
# KgmWasm
## 构建
在 Linux 环境下执行 `bash build-wasm` 即可构建。
## Build
Linux environment required. Build wasm binary by execute `bash build-wasm`.

41
src/KgmWasm/build-wasm Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -e
pushd "$(realpath "$(dirname "$0")")"
CURR_DIR="${PWD}"
BUILD_TYPE="$1"
if [ -z "$BUILD_TYPE" ]; then
BUILD_TYPE=Release
fi
# CI: already had emsdk installed.
if ! command -v emcc; then
if [ ! -d ../../build/emsdk ]; then
git clone https://github.com/emscripten-core/emsdk.git ../../build/emsdk
fi
pushd ../../build/emsdk
./emsdk install 3.0.0
./emsdk activate 3.0.0
source ./emsdk_env.sh
popd # ../../build/emsdk
fi
mkdir -p build/wasm
pushd build/wasm
emcmake cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" ../..
make -j
TARGET_FILES="
KgmLegacy.js
KgmWasm.js
KgmWasm.wasm
KgmWasmBundle.js
"
cp $TARGET_FILES "${CURR_DIR}/"
popd # build/wasm
popd

112
src/KgmWasm/kgm.hpp Normal file
View File

@ -0,0 +1,112 @@
#include <vector>
std::vector<uint8_t> VprHeader = {
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
0x91, 0xAA, 0xBD, 0xD0, 0x7A, 0xF5, 0x36, 0x31 };
std::vector<uint8_t> KgmHeader = {
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14 };
std::vector<uint8_t> VprMaskDiff = {
0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11, 0x00 };
std::vector<uint8_t> MaskV2;
std::vector<uint8_t> table1 = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x21, 0x01, 0x61, 0x01, 0x21, 0x01, 0xe1, 0x01, 0x21, 0x01, 0x61, 0x01, 0x21, 0x01,
0xd2, 0x23, 0x02, 0x02, 0x42, 0x42, 0x02, 0x02, 0xc2, 0xc2, 0x02, 0x02, 0x42, 0x42, 0x02, 0x02,
0xd3, 0xd3, 0x02, 0x03, 0x63, 0x43, 0x63, 0x03, 0xe3, 0xc3, 0xe3, 0x03, 0x63, 0x43, 0x63, 0x03,
0x94, 0xb4, 0x94, 0x65, 0x04, 0x04, 0x04, 0x04, 0x84, 0x84, 0x84, 0x84, 0x04, 0x04, 0x04, 0x04,
0x95, 0x95, 0x95, 0x95, 0x04, 0x05, 0x25, 0x05, 0xe5, 0x85, 0xa5, 0x85, 0xe5, 0x05, 0x25, 0x05,
0xd6, 0xb6, 0x96, 0xb6, 0xd6, 0x27, 0x06, 0x06, 0xc6, 0xc6, 0x86, 0x86, 0xc6, 0xc6, 0x06, 0x06,
0xd7, 0xd7, 0x97, 0x97, 0xd7, 0xd7, 0x06, 0x07, 0xe7, 0xc7, 0xe7, 0x87, 0xe7, 0xc7, 0xe7, 0x07,
0x18, 0x38, 0x18, 0x78, 0x18, 0x38, 0x18, 0xe9, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x08, 0x09, 0x29, 0x09, 0x69, 0x09, 0x29, 0x09,
0xda, 0x3a, 0x1a, 0x3a, 0x5a, 0x3a, 0x1a, 0x3a, 0xda, 0x2b, 0x0a, 0x0a, 0x4a, 0x4a, 0x0a, 0x0a,
0xdb, 0xdb, 0x1b, 0x1b, 0x5b, 0x5b, 0x1b, 0x1b, 0xdb, 0xdb, 0x0a, 0x0b, 0x6b, 0x4b, 0x6b, 0x0b,
0x9c, 0xbc, 0x9c, 0x7c, 0x1c, 0x3c, 0x1c, 0x7c, 0x9c, 0xbc, 0x9c, 0x6d, 0x0c, 0x0c, 0x0c, 0x0c,
0x9d, 0x9d, 0x9d, 0x9d, 0x1d, 0x1d, 0x1d, 0x1d, 0x9d, 0x9d, 0x9d, 0x9d, 0x0c, 0x0d, 0x2d, 0x0d,
0xde, 0xbe, 0x9e, 0xbe, 0xde, 0x3e, 0x1e, 0x3e, 0xde, 0xbe, 0x9e, 0xbe, 0xde, 0x2f, 0x0e, 0x0e,
0xdf, 0xdf, 0x9f, 0x9f, 0xdf, 0xdf, 0x1f, 0x1f, 0xdf, 0xdf, 0x9f, 0x9f, 0xdf, 0xdf, 0x0e, 0x0f,
0x00, 0x20, 0x00, 0x60, 0x00, 0x20, 0x00, 0xe0, 0x00, 0x20, 0x00, 0x60, 0x00, 0x20, 0x00, 0xf1
};
std::vector<uint8_t> table2 = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x23, 0x01, 0x67, 0x01, 0x23, 0x01, 0xef, 0x01, 0x23, 0x01, 0x67, 0x01, 0x23, 0x01,
0xdf, 0x21, 0x02, 0x02, 0x46, 0x46, 0x02, 0x02, 0xce, 0xce, 0x02, 0x02, 0x46, 0x46, 0x02, 0x02,
0xde, 0xde, 0x02, 0x03, 0x65, 0x47, 0x65, 0x03, 0xed, 0xcf, 0xed, 0x03, 0x65, 0x47, 0x65, 0x03,
0x9d, 0xbf, 0x9d, 0x63, 0x04, 0x04, 0x04, 0x04, 0x8c, 0x8c, 0x8c, 0x8c, 0x04, 0x04, 0x04, 0x04,
0x9c, 0x9c, 0x9c, 0x9c, 0x04, 0x05, 0x27, 0x05, 0xeb, 0x8d, 0xaf, 0x8d, 0xeb, 0x05, 0x27, 0x05,
0xdb, 0xbd, 0x9f, 0xbd, 0xdb, 0x25, 0x06, 0x06, 0xca, 0xca, 0x8e, 0x8e, 0xca, 0xca, 0x06, 0x06,
0xda, 0xda, 0x9e, 0x9e, 0xda, 0xda, 0x06, 0x07, 0xe9, 0xcb, 0xe9, 0x8f, 0xe9, 0xcb, 0xe9, 0x07,
0x19, 0x3b, 0x19, 0x7f, 0x19, 0x3b, 0x19, 0xe7, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x08, 0x09, 0x2b, 0x09, 0x6f, 0x09, 0x2b, 0x09,
0xd7, 0x39, 0x1b, 0x39, 0x5f, 0x39, 0x1b, 0x39, 0xd7, 0x29, 0x0a, 0x0a, 0x4e, 0x4e, 0x0a, 0x0a,
0xd6, 0xd6, 0x1a, 0x1a, 0x5e, 0x5e, 0x1a, 0x1a, 0xd6, 0xd6, 0x0a, 0x0b, 0x6d, 0x4f, 0x6d, 0x0b,
0x95, 0xb7, 0x95, 0x7b, 0x1d, 0x3f, 0x1d, 0x7b, 0x95, 0xb7, 0x95, 0x6b, 0x0c, 0x0c, 0x0c, 0x0c,
0x94, 0x94, 0x94, 0x94, 0x1c, 0x1c, 0x1c, 0x1c, 0x94, 0x94, 0x94, 0x94, 0x0c, 0x0d, 0x2f, 0x0d,
0xd3, 0xb5, 0x97, 0xb5, 0xd3, 0x3d, 0x1f, 0x3d, 0xd3, 0xb5, 0x97, 0xb5, 0xd3, 0x2d, 0x0e, 0x0e,
0xd2, 0xd2, 0x96, 0x96, 0xd2, 0xd2, 0x1e, 0x1e, 0xd2, 0xd2, 0x96, 0x96, 0xd2, 0xd2, 0x0e, 0x0f,
0x00, 0x22, 0x00, 0x66, 0x00, 0x22, 0x00, 0xee, 0x00, 0x22, 0x00, 0x66, 0x00, 0x22, 0x00, 0xfe
};
std::vector<uint8_t> 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,
};
uint8_t getMask(size_t pos) {
size_t offset = pos >> 4;
uint8_t value = 0;
while (offset >= 0x11) {
value ^= table1[offset % 272];
offset >>= 4;
value ^= table2[offset % 272];
offset >>= 4;
}
return MaskV2PreDef[pos % 272] ^ value;
}
std::vector<uint8_t> key(17);
bool isVpr = false;
size_t PreDec(uint8_t* fileData, size_t size, bool iV) {
uint32_t headerLen = *(uint32_t*)(fileData + 0x10);
memcpy(key.data(), (fileData + 0x1C), 0x10);
key[16] = 0;
isVpr = iV;
return headerLen;
}
void Decrypt(uint8_t* fileData, size_t size, size_t offset) {
for (size_t i = 0; i < size; ++i) {
uint8_t med8 = key[(i + offset) % 17] ^ fileData[i];
med8 ^= (med8 & 0xf) << 4;
uint8_t msk8 = getMask(i + offset);
msk8 ^= (msk8 & 0xf) << 4;
fileData[i] = med8 ^ msk8;
if (isVpr) {
fileData[i] ^= VprMaskDiff[(i + offset) % 17];
}
}
}

View File

@ -0,0 +1,65 @@
# CMakeList.txt : CMake project for QmcWasm, include source and define
# project specific logic here.
#
cmake_minimum_required (VERSION 3.8)
project ("QmcWasm")
set(CMAKE_CXX_STANDARD 14)
include_directories(
$<TARGET_PROPERTY:INTERFACE_INCLUDE_DIRECTORIES>
)
# Add source to this project's executable.
set(RUNTIME_METHODS_LIST
getValue
writeArrayToMemory
UTF8ToString
)
list(JOIN RUNTIME_METHODS_LIST "," RUNTIME_METHODS)
set(EMSCRIPTEN_FLAGS
"--bind"
"-s NO_DYNAMIC_EXECUTION=1"
"-s MODULARIZE=1"
"-s EXPORT_NAME=QmcCryptoModule"
"-s EXPORTED_RUNTIME_METHODS=${RUNTIME_METHODS}"
)
set(EMSCRIPTEN_LEGACY_FLAGS
${EMSCRIPTEN_FLAGS}
"-s WASM=0"
"--memory-init-file 0"
)
set(EMSCRIPTEN_WASM_BUNDLE_FLAGS
${EMSCRIPTEN_FLAGS}
"-s SINGLE_FILE=1"
)
list(JOIN EMSCRIPTEN_FLAGS " " EMSCRIPTEN_FLAGS_STR)
list(JOIN EMSCRIPTEN_LEGACY_FLAGS " " EMSCRIPTEN_LEGACY_FLAGS_STR)
list(JOIN EMSCRIPTEN_WASM_BUNDLE_FLAGS " " EMSCRIPTEN_WASM_BUNDLE_FLAGS_STR)
# Define projects config
set(WASM_SOURCES
"QmcWasm.cpp"
)
add_executable(QmcWasm ${WASM_SOURCES})
set_target_properties(
QmcWasm
PROPERTIES LINK_FLAGS ${EMSCRIPTEN_FLAGS_STR}
)
add_executable(QmcWasmBundle ${WASM_SOURCES})
set_target_properties(
QmcWasmBundle
PROPERTIES LINK_FLAGS ${EMSCRIPTEN_WASM_BUNDLE_FLAGS_STR}
)
add_executable(QmcLegacy ${WASM_SOURCES})
set_target_properties(
QmcLegacy
PROPERTIES LINK_FLAGS ${EMSCRIPTEN_LEGACY_FLAGS_STR}
)

57
src/QmcWasm/QmcWasm.cpp Normal file
View File

@ -0,0 +1,57 @@
// QmcWasm.cpp : Defines the entry point for the application.
//
#include "QmcWasm.h"
#include "qmc.hpp"
#include <stddef.h>
#include <string.h>
std::string err = "";
std::string sid = "";
QmcDecode e;
int preDec(uintptr_t blob, size_t blobSize, std::string ext)
{
if (!e.SetBlob((uint8_t*)blob, blobSize))
{
err = "cannot allocate memory";
return -1;
}
int tailSize = e.PreDecode(ext);
if (e.error != "")
{
err = e.error;
return -1;
}
sid = e.songId;
return tailSize;
}
size_t decBlob(uintptr_t blob, size_t blobSize, size_t offset)
{
if (!e.SetBlob((uint8_t*)blob, blobSize))
{
err = "cannot allocate memory";
return 0;
}
std::vector<uint8_t> decData = e.Decode(offset);
if (e.error != "")
{
err = e.error;
return 0;
}
memcpy((uint8_t*)blob, decData.data(), decData.size());
return decData.size();
}
std::string getErr()
{
return err;
}
std::string getSongId()
{
return sid;
}

23
src/QmcWasm/QmcWasm.h Normal file
View File

@ -0,0 +1,23 @@
// QmcWasm.h : Include file for standard system include files,
// or project specific include files.
#pragma once
#include <emscripten/bind.h>
#include <string>
namespace em = emscripten;
int preDec(uintptr_t blob, size_t blobSize, std::string ext);
size_t decBlob(uintptr_t blob, size_t blobSize, size_t offset);
std::string getErr();
std::string getSongId();
EMSCRIPTEN_BINDINGS(QmcCrypto)
{
em::function("getErr", &getErr);
em::function("getSongId", &getSongId);
em::function("preDec", &preDec, em::allow_raw_pointers());
em::function("decBlob", &decBlob, em::allow_raw_pointers());
}

9
src/QmcWasm/README.md Normal file
View File

@ -0,0 +1,9 @@
# QmcWasm
## 构建
在 Linux 环境下执行 `bash build-wasm` 即可构建。
## Build
Linux environment required. Build wasm binary by execute `bash build-wasm`.

289
src/QmcWasm/TencentTea.hpp Normal file
View File

@ -0,0 +1,289 @@
#ifndef QQMUSIC_CPP_TENCENTTEA_HPP
#define QQMUSIC_CPP_TENCENTTEA_HPP
#include <cstdlib>
#include <cstdio>
#include <cstdint>
#include <vector>
#include <time.h>
#include <arpa/inet.h>
const uint32_t DELTA = 0x9e3779b9;
#define ROUNDS 32
#define SALT_LEN 2
#define ZERO_LEN 7
void TeaDecryptECB(uint8_t* src, uint8_t* dst, std::vector<uint8_t> key, size_t rounds = ROUNDS) {
if (key.size() != 16 || (rounds & 1) != 0)
{
return;
}
uint32_t y, z, sum;
uint32_t k[4];
int i;
//now encrypted buf is TCP/IP-endian;
//TCP/IP network byte order (which is big-endian).
y = ntohl(*((uint32_t*)src));
z = ntohl(*((uint32_t*)(src + 4)));
//std::cout << ntohl(0x0a3aea41);
for (i = 0; i < 4; i++) {
//key is TCP/IP-endian;
k[i] = ntohl(*((uint32_t*)(key.data() + i * 4)));
}
sum = (DELTA * rounds);
for (i = 0; i < rounds; i++) {
z -= ((y << 4) + k[2]) ^ (y + sum) ^ ((y >> 5) + k[3]);
y -= ((z << 4) + k[0]) ^ (z + sum) ^ ((z >> 5) + k[1]);
sum -= DELTA;
}
*((uint32_t*)dst) = ntohl(y);
*((uint32_t*)(dst + 4)) = ntohl(z);
//now plain-text is TCP/IP-endian;
}
void TeaEncryptECB(uint8_t* src, uint8_t* dst, std::vector<uint8_t> key, size_t rounds = ROUNDS) {
if (key.size() != 16 || (rounds & 1) != 0)
{
return;
}
uint32_t y, z, sum;
uint32_t k[4];
int i;
//now encrypted buf is TCP/IP-endian;
//TCP/IP network byte order (which is big-endian).
y = ntohl(*((uint32_t*)src));
z = ntohl(*((uint32_t*)(src + 4)));
//std::cout << ntohl(0x0a3aea41);
for (i = 0; i < 4; i++) {
//key is TCP/IP-endian;
k[i] = ntohl(*((uint32_t*)(key.data() + i * 4)));
}
sum = 0;
for (i = 0; i < rounds; i++) {
sum += DELTA;
y += ((z << 4) + k[0]) ^ (z + sum) ^ ((z >> 5) + k[1]);
z += ((y << 4) + k[2]) ^ (y + sum) ^ ((y >> 5) + k[3]);
}
*((uint32_t*)dst) = ntohl(y);
*((uint32_t*)(dst + 4)) = ntohl(z);
//now plain-text is TCP/IP-endian;
}
/*pKey为16byte*/
/*
:nInBufLen为需加密的明文部分(Body);
:(8byte的倍数);
*/
/*TEA加密算法,CBC模式*/
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
int encryptTencentTeaLen(int nInBufLen)
{
int nPadSaltBodyZeroLen/*PadLen(1byte)+Salt+Body+Zero的长度*/;
int nPadlen;
/*根据Body长度计算PadLen,最小必需长度必需为8byte的整数倍*/
nPadSaltBodyZeroLen = nInBufLen/*Body长度*/ + 1 + SALT_LEN + ZERO_LEN/*PadLen(1byte)+Salt(2byte)+Zero(7byte)*/;
if ((nPadlen = nPadSaltBodyZeroLen % 8)) /*len=nSaltBodyZeroLen%8*/
{
/*模8余0需补0,余1补7,余2补6,...,余7补1*/
nPadlen = 8 - nPadlen;
}
return nPadlen;
}
/*pKey为16byte*/
/*
:pInBuf为需加密的明文部分(Body),nInBufLen为pInBuf长度;
:pOutBuf为密文格式,pOutBufLen为pOutBuf的长度是8byte的倍数;
*/
/*TEA加密算法,CBC模式*/
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
bool encryptTencentTea(std::vector<uint8_t> inBuf, std::vector<uint8_t> key, std::vector<uint8_t> &outBuf)
{
srand(time(0));
int nPadlen = encryptTencentTeaLen(inBuf.size());
size_t ivCrypt;
std::vector<uint8_t> srcBuf;
srcBuf.resize(8);
std::vector<uint8_t> ivPlain;
ivPlain.resize(8);
int tmpIdx, i, j;
/*加密第一块数据(8byte),取前面10byte*/
srcBuf[0] = (((char)rand()) & 0x0f8)/*最低三位存PadLen,清零*/ | (char)nPadlen;
tmpIdx = 1; /*tmpIdx指向srcBuf下一个位置*/
while (nPadlen--) srcBuf[tmpIdx++] = (char)rand(); /*Padding*/
/*come here, tmpIdx must <= 8*/
for (i = 0; i < 8; i++) ivPlain[i] = 0;
ivCrypt = 0;//ivPlain /*make zero iv*/
auto outBufPos = 0; /*init outBufPos*/
#define cryptBlock {\
/*tmpIdx==8*/\
outBuf.resize(outBuf.size() + 8);\
for (j = 0; j < 8; j++) /*加密前异或前8个byte的密文(iv_crypt指向的)*/\
srcBuf[j] ^= outBuf[j + ivCrypt];\
/*pOutBuffer、pInBuffer均为8byte, pKey为16byte*/\
/*加密*/\
TeaEncryptECB(srcBuf.data(), outBuf.data()+outBufPos, key, 16);\
for (j = 0; j < 8; j++) /*加密后异或前8个byte的明文(iv_plain指向的)*/\
outBuf[j + outBufPos] ^= ivPlain[j];\
/*保存当前的iv_plain*/\
for (j = 0; j < 8; j++) ivPlain[j] = srcBuf[j];\
/*更新iv_crypt*/\
tmpIdx = 0;\
ivCrypt = outBufPos;\
outBufPos += 8;\
}
for (i = 1; i <= SALT_LEN;) /*Salt(2byte)*/
{
if (tmpIdx < 8)
{
srcBuf[tmpIdx++] = (char)rand();
i++; /*i inc in here*/
}
if (tmpIdx == 8)
{
cryptBlock
}
}
/*tmpIdx指向srcBuf下一个位置*/
auto inBufPos = 0;
while (inBufPos < inBuf.size())
{
if (tmpIdx < 8)
{
srcBuf[tmpIdx++] = inBuf[inBufPos];
inBufPos++;
}
if (tmpIdx == 8)
{
cryptBlock
}
}
/*tmpIdx指向srcBuf下一个位置*/
for (i = 1; i <= ZERO_LEN;)
{
if (tmpIdx < 8)
{
srcBuf[tmpIdx++] = 0;
i++; //i inc in here
}
if (tmpIdx == 8)
{
cryptBlock
}
}
return true;
#undef cryptBlock
}
bool decryptTencentTea(std::vector<uint8_t> inBuf, std::vector<uint8_t> key, std::vector<uint8_t> &out) {
if (inBuf.size() % 8 != 0) {
return false;
//inBuf size not a multiple of the block size
}
if (inBuf.size() < 16) {
return false;
//inBuf size too small
}
std::vector<uint8_t> tmpBuf;
tmpBuf.resize(8);
TeaDecryptECB(inBuf.data(), tmpBuf.data(), key, 16);
auto nPadLen = tmpBuf[0] & 0x7; //只要最低三位
/*密文格式:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/
auto outLen = inBuf.size() - 1 /*PadLen*/ - nPadLen - SALT_LEN - ZERO_LEN;
std::vector<uint8_t> outBuf;
outBuf.resize(outLen);
std::vector<uint8_t> ivPrev;
ivPrev.resize(8);
std::vector<uint8_t> ivCur;
ivCur.resize(8);
for (size_t i = 0; i < 8; i++)
{
ivCur[i] = inBuf[i]; // init iv
}
auto inBufPos = 8;
// 跳过 Padding Len 和 Padding
auto tmpIdx = 1 + nPadLen;
// CBC IV 处理
#define cryptBlock {\
ivPrev = ivCur;\
for (size_t k = inBufPos; k < inBufPos + 8; k++)\
{\
ivCur[k - inBufPos] = inBuf[k];\
}\
for (size_t j = 0; j < 8; j++) {\
tmpBuf[j] ^= ivCur[j];\
}\
TeaDecryptECB(tmpBuf.data(), tmpBuf.data(), key, 16);\
inBufPos += 8;\
tmpIdx = 0;\
}
// 跳过 Salt
for (size_t i = 1; i <= SALT_LEN; ) {
if (tmpIdx < 8) {
tmpIdx++;
i++;
}
else {
cryptBlock
}
}
// 还原明文
auto outBufPos = 0;
while (outBufPos < outLen) {
if (tmpIdx < 8) {
outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx];
outBufPos++;
tmpIdx++;
}
else {
cryptBlock
}
}
// 校验Zero
for (size_t i = 1; i <= ZERO_LEN; i++) {
if (tmpBuf[i] != ivPrev[i]) {
return false;
//zero check failed
}
}
out = outBuf;
return true;
#undef cryptBlock
}
#endif //QQMUSIC_CPP_TENCENTTEA_HPP

207
src/QmcWasm/base64.hpp Normal file
View File

@ -0,0 +1,207 @@
//
// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/beast
//
/*
Portions from http://www.adp-gmbh.ch/cpp/common/base64.html
Copyright notice:
base64.cpp and base64.h
Copyright (C) 2004-2008 Rene Nyffenegger
This source code is provided 'as-is', without any express or implied
warranty. In no event will the author be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this source code must not be misrepresented; you must not
claim that you wrote the original source code. If you use this source code
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original source code.
3. This notice may not be removed or altered from any source distribution.
Rene Nyffenegger rene.nyffenegger@adp-gmbh.ch
*/
#ifndef BASE64_HPP
#define BASE64_HPP
#include <cctype>
#include <string>
#include <utility>
namespace base64 {
/// Returns max chars needed to encode a base64 string
std::size_t constexpr
encoded_size(std::size_t n)
{
return 4 * ((n + 2) / 3);
}
/// Returns max bytes needed to decode a base64 string
inline
std::size_t constexpr
decoded_size(std::size_t n)
{
return n / 4 * 3; // requires n&3==0, smaller
}
char const*
get_alphabet()
{
static char constexpr tab[] = {
"ABCDEFGHIJKLMNOP"
"QRSTUVWXYZabcdef"
"ghijklmnopqrstuv"
"wxyz0123456789+/"
};
return &tab[0];
}
signed char const*
get_inverse()
{
static signed char constexpr tab[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0-15
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16-31
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, // 32-47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 48-63
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 64-79
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, // 80-95
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 96-111
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, // 112-127
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128-143
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 144-159
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 160-175
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 176-191
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 192-207
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 208-223
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 224-239
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 // 240-255
};
return &tab[0];
}
/** Encode a series of octets as a padded, base64 string.
The resulting string will not be null terminated.
@par Requires
The memory pointed to by `out` points to valid memory
of at least `encoded_size(len)` bytes.
@return The number of characters written to `out`. This
will exclude any null termination.
*/
std::size_t
encode(void* dest, void const* src, std::size_t len)
{
char* out = static_cast<char*>(dest);
char const* in = static_cast<char const*>(src);
auto const tab = base64::get_alphabet();
for (auto n = len / 3; n--;)
{
*out++ = tab[(in[0] & 0xfc) >> 2];
*out++ = tab[((in[0] & 0x03) << 4) + ((in[1] & 0xf0) >> 4)];
*out++ = tab[((in[2] & 0xc0) >> 6) + ((in[1] & 0x0f) << 2)];
*out++ = tab[in[2] & 0x3f];
in += 3;
}
switch (len % 3)
{
case 2:
*out++ = tab[(in[0] & 0xfc) >> 2];
*out++ = tab[((in[0] & 0x03) << 4) + ((in[1] & 0xf0) >> 4)];
*out++ = tab[(in[1] & 0x0f) << 2];
*out++ = '=';
break;
case 1:
*out++ = tab[(in[0] & 0xfc) >> 2];
*out++ = tab[((in[0] & 0x03) << 4)];
*out++ = '=';
*out++ = '=';
break;
case 0:
break;
}
return out - static_cast<char*>(dest);
}
/** Decode a padded base64 string into a series of octets.
@par Requires
The memory pointed to by `out` points to valid memory
of at least `decoded_size(len)` bytes.
@return The number of octets written to `out`, and
the number of characters read from the input string,
expressed as a pair.
*/
std::pair<std::size_t, std::size_t>
decode(void* dest, char const* src, std::size_t len)
{
char* out = static_cast<char*>(dest);
auto in = reinterpret_cast<unsigned char const*>(src);
unsigned char c3[3], c4[4];
int i = 0;
int j = 0;
auto const inverse = base64::get_inverse();
while (len-- && *in != '=')
{
auto const v = inverse[*in];
if (v == -1)
break;
++in;
c4[i] = v;
if (++i == 4)
{
c3[0] = (c4[0] << 2) + ((c4[1] & 0x30) >> 4);
c3[1] = ((c4[1] & 0xf) << 4) + ((c4[2] & 0x3c) >> 2);
c3[2] = ((c4[2] & 0x3) << 6) + c4[3];
for (i = 0; i < 3; i++)
*out++ = c3[i];
i = 0;
}
}
if (i)
{
c3[0] = (c4[0] << 2) + ((c4[1] & 0x30) >> 4);
c3[1] = ((c4[1] & 0xf) << 4) + ((c4[2] & 0x3c) >> 2);
c3[2] = ((c4[2] & 0x3) << 6) + c4[3];
for (j = 0; j < i - 1; j++)
*out++ = c3[j];
}
return { out - static_cast<char*>(dest),
in - reinterpret_cast<unsigned char const*>(src) };
}
} // base64
#endif

41
src/QmcWasm/build-wasm Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -e
pushd "$(realpath "$(dirname "$0")")"
CURR_DIR="${PWD}"
BUILD_TYPE="$1"
if [ -z "$BUILD_TYPE" ]; then
BUILD_TYPE=Release
fi
# CI: already had emsdk installed.
if ! command -v emcc; then
if [ ! -d ../../build/emsdk ]; then
git clone https://github.com/emscripten-core/emsdk.git ../../build/emsdk
fi
pushd ../../build/emsdk
./emsdk install 3.0.0
./emsdk activate 3.0.0
source ./emsdk_env.sh
popd # ../../build/emsdk
fi
mkdir -p build/wasm
pushd build/wasm
emcmake cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" ../..
make -j
TARGET_FILES="
QmcLegacy.js
QmcWasm.js
QmcWasm.wasm
QmcWasmBundle.js
"
cp $TARGET_FILES "${CURR_DIR}/"
popd # build/wasm
popd

230
src/QmcWasm/qmc.hpp Normal file
View File

@ -0,0 +1,230 @@
#include <string.h>
#include <cmath>
#include <vector>
#include <arpa/inet.h>
#include "qmc_key.hpp"
#include "qmc_cipher.hpp"
class QmcDecode {
private:
std::vector<uint8_t> blobData;
std::vector<uint8_t> rawKeyBuf;
std::string cipherType = "";
size_t dataOffset = 0;
size_t keySize = 0;
int mediaVer = 0;
std::string checkType(std::string fn) {
if (fn.find(".qmc") < fn.size() || fn.find(".m") < fn.size())
{
std::string buf_tag = "";
for (int i = 4; i > 0; --i)
{
buf_tag += *((char*)blobData.data() + blobData.size() - i);
}
if (buf_tag == "QTag")
{
keySize = ntohl(*(uint32_t*)(blobData.data() + blobData.size() - 8));
return "QTag";
}
else if (buf_tag == "STag")
{
return "STag";
}
else
{
keySize = (*(uint32_t*)(blobData.data() + blobData.size() - 4));
if (keySize < 0x400)
{
return "Map/RC4";
}
else
{
keySize = 0;
return "Static";
}
}
}
else if (fn.find(".cache") < fn.size())
{
return "cache";
}
else if (fn.find(".tm") < fn.size())
{
return "ios";
}
else
{
return "invalid";
}
}
bool parseRawKeyQTag() {
std::string ketStr = "";
std::string::size_type index = 0;
ketStr.append((char*)rawKeyBuf.data(), rawKeyBuf.size());
index = ketStr.find(",", 0);
if (index != std::string::npos)
{
rawKeyBuf.resize(index);
}
else
{
return false;
}
ketStr = ketStr.substr(index + 1);
index = ketStr.find(",", 0);
if (index != std::string::npos)
{
this->songId = ketStr.substr(0, index);
}
else
{
return false;
}
ketStr = ketStr.substr(index + 1);
index = ketStr.find(",", 0);
if (index == std::string::npos)
{
this->mediaVer = std::stoi(ketStr);
}
else
{
return false;
}
return true;
}
bool readRawKey(size_t tailSize) {
// get raw key data length
rawKeyBuf.resize(keySize);
if (rawKeyBuf.size() != keySize) {
return false;
}
for (size_t i = 0; i < keySize; i++)
{
rawKeyBuf[i] = blobData[i + blobData.size() - (tailSize + keySize)];
}
return true;
}
void DecodeStatic();
void DecodeMapRC4();
void DecodeCache();
void DecodeTm();
public:
bool SetBlob(uint8_t* blob, size_t blobSize) {
blobData.resize(blobSize);
if (blobData.size() != blobSize) {
return false;
}
memcpy(blobData.data(), blob, blobSize);
return true;
}
int PreDecode(std::string ext) {
cipherType = checkType(ext);
size_t tailSize = 0;
if (cipherType == "invalid" || cipherType == "STag") {
error = "file is invalid or not supported (Please downgrade your app).";
return -1;
}
if (cipherType == "QTag") {
tailSize = 8;
}
else if (cipherType == "Map/RC4") {
tailSize = 4;
}
if (keySize > 0) {
if (!readRawKey(tailSize)) {
error = "cannot read embedded key from file";
return -1;
}
if (tailSize == 8) {
cipherType = "Map/RC4";
if (!parseRawKeyQTag()) {
error = "cannot parse embedded key";
return -1;
}
}
std::vector<uint8_t> tmp;
if (!QmcDecryptKey(rawKeyBuf, tmp)) {
error = "cannot decrypt embedded key";
return -1;
}
rawKeyBuf = tmp;
}
return keySize + tailSize;
}
std::vector<uint8_t> Decode(size_t offset);
std::string songId = "";
std::string error = "";
};
void QmcDecode::DecodeStatic()
{
QmcStaticCipher sc;
sc.proc(blobData, dataOffset);
}
void QmcDecode::DecodeMapRC4() {
if (rawKeyBuf.size() > 300)
{
QmcRC4Cipher c(rawKeyBuf, 2);
c.proc(blobData, dataOffset);
}
else
{
QmcMapCipher c(rawKeyBuf, 2);
c.proc(blobData, dataOffset);
}
}
void QmcDecode::DecodeCache()
{
for (size_t i = 0; i < blobData.size(); i++) {
blobData[i] ^= 0xf4;
blobData[i] = ((blobData[i] & 0b00111111) << 2) | (blobData[i] >> 6); // rol 2
}
}
void QmcDecode::DecodeTm()
{
uint8_t const TM_HEADER[] = { 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70 };
for (size_t cur = dataOffset, i = 0; cur < 8 && i < blobData.size(); ++cur, ++i) {
blobData[i] = TM_HEADER[dataOffset];
}
}
std::vector<uint8_t> QmcDecode::Decode(size_t offset)
{
dataOffset = offset;
if (cipherType == "Map/RC4")
{
DecodeMapRC4();
}
else if (cipherType == "Static")
{
DecodeStatic();
}
else if (cipherType == "cache")
{
DecodeCache();
}
else if (cipherType == "ios")
{
DecodeTm();
}
else {
error = "File is invalid or encryption type is not supported.";
}
return blobData;
}

290
src/QmcWasm/qmc_cipher.hpp Normal file
View File

@ -0,0 +1,290 @@
#include <cstdint>
#include <vector>
class QmcStaticCipher {
private:
uint8_t staticCipherBox[256] = {
0x77, 0x48, 0x32, 0x73, 0xDE, 0xF2, 0xC0, 0xC8, //0x00
0x95, 0xEC, 0x30, 0xB2, 0x51, 0xC3, 0xE1, 0xA0, //0x08
0x9E, 0xE6, 0x9D, 0xCF, 0xFA, 0x7F, 0x14, 0xD1, //0x10
0xCE, 0xB8, 0xDC, 0xC3, 0x4A, 0x67, 0x93, 0xD6, //0x18
0x28, 0xC2, 0x91, 0x70, 0xCA, 0x8D, 0xA2, 0xA4, //0x20
0xF0, 0x08, 0x61, 0x90, 0x7E, 0x6F, 0xA2, 0xE0, //0x28
0xEB, 0xAE, 0x3E, 0xB6, 0x67, 0xC7, 0x92, 0xF4, //0x30
0x91, 0xB5, 0xF6, 0x6C, 0x5E, 0x84, 0x40, 0xF7, //0x38
0xF3, 0x1B, 0x02, 0x7F, 0xD5, 0xAB, 0x41, 0x89, //0x40
0x28, 0xF4, 0x25, 0xCC, 0x52, 0x11, 0xAD, 0x43, //0x48
0x68, 0xA6, 0x41, 0x8B, 0x84, 0xB5, 0xFF, 0x2C, //0x50
0x92, 0x4A, 0x26, 0xD8, 0x47, 0x6A, 0x7C, 0x95, //0x58
0x61, 0xCC, 0xE6, 0xCB, 0xBB, 0x3F, 0x47, 0x58, //0x60
0x89, 0x75, 0xC3, 0x75, 0xA1, 0xD9, 0xAF, 0xCC, //0x68
0x08, 0x73, 0x17, 0xDC, 0xAA, 0x9A, 0xA2, 0x16, //0x70
0x41, 0xD8, 0xA2, 0x06, 0xC6, 0x8B, 0xFC, 0x66, //0x78
0x34, 0x9F, 0xCF, 0x18, 0x23, 0xA0, 0x0A, 0x74, //0x80
0xE7, 0x2B, 0x27, 0x70, 0x92, 0xE9, 0xAF, 0x37, //0x88
0xE6, 0x8C, 0xA7, 0xBC, 0x62, 0x65, 0x9C, 0xC2, //0x90
0x08, 0xC9, 0x88, 0xB3, 0xF3, 0x43, 0xAC, 0x74, //0x98
0x2C, 0x0F, 0xD4, 0xAF, 0xA1, 0xC3, 0x01, 0x64, //0xA0
0x95, 0x4E, 0x48, 0x9F, 0xF4, 0x35, 0x78, 0x95, //0xA8
0x7A, 0x39, 0xD6, 0x6A, 0xA0, 0x6D, 0x40, 0xE8, //0xB0
0x4F, 0xA8, 0xEF, 0x11, 0x1D, 0xF3, 0x1B, 0x3F, //0xB8
0x3F, 0x07, 0xDD, 0x6F, 0x5B, 0x19, 0x30, 0x19, //0xC0
0xFB, 0xEF, 0x0E, 0x37, 0xF0, 0x0E, 0xCD, 0x16, //0xC8
0x49, 0xFE, 0x53, 0x47, 0x13, 0x1A, 0xBD, 0xA4, //0xD0
0xF1, 0x40, 0x19, 0x60, 0x0E, 0xED, 0x68, 0x09, //0xD8
0x06, 0x5F, 0x4D, 0xCF, 0x3D, 0x1A, 0xFE, 0x20, //0xE0
0x77, 0xE4, 0xD9, 0xDA, 0xF9, 0xA4, 0x2B, 0x76, //0xE8
0x1C, 0x71, 0xDB, 0x00, 0xBC, 0xFD, 0x0C, 0x6C, //0xF0
0xA5, 0x47, 0xF7, 0xF6, 0x00, 0x79, 0x4A, 0x11 //0xF8
};
uint8_t getMask(size_t offset) {
if (offset > 0x7fff) offset %= 0x7fff;
return staticCipherBox[(offset * offset + 27) & 0xff];
}
public:
void proc(std::vector<uint8_t>& buf, size_t offset) {
for (size_t i = 0; i < buf.size(); i++) {
buf[i] ^= getMask(offset + i);
}
}
};
class QmcMapCipher {
private:
std::vector<uint8_t> key;
uint8_t rotate(uint8_t value, size_t bits) {
auto rotate = (bits + 4) % 8;
auto left = value << rotate;
auto right = value >> rotate;
return (left | right) & 0xff;
}
uint8_t getMask(size_t offset) {
if (offset > 0x7fff) offset %= 0x7fff;
const auto idx = (offset * offset + 71214) % key.size();
return rotate(key[idx], idx & 0x7);
}
public:
QmcMapCipher(std::vector<uint8_t> &argKey, short operation) {
if (operation == 2)
{
if (argKey.size() == 0) {
return;
}
}
else if (operation == 1)
{
const char WordList[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
srand(time(0));
uint32_t number = 0;
while (number > 300 || number == 0)
{
number = rand();
}
argKey.resize(number);
for (int i = 0; i < argKey.size(); i++) {
number = rand();
argKey[i] = WordList[number % 62];
}
}
else
{
return;
}
key = argKey;
}
void proc(std::vector<uint8_t>& buf, size_t offset) {
for (size_t i = 0; i < buf.size(); i++) {
buf[i] ^= getMask(offset + i);
}
}
};
class QmcRC4Cipher {
public:
void proc(std::vector<uint8_t>& buf, size_t offset) {
// Macro: common code after each process
#define postProcess(len) \
{ \
toProcess -= len; \
processed += len; \
offset += len; \
/* no more data */ \
if (toProcess == 0) { \
return; \
} \
}
size_t toProcess = buf.size();
size_t processed = 0;
std::vector<uint8_t> tmpbuf;
// 前 128 字节使用不同的解密方案
if (offset < FIRST_SEGMENT_SIZE) {
size_t len_segment = std::min(FIRST_SEGMENT_SIZE - offset, buf.size());
tmpbuf.resize(len_segment);
for (size_t i = 0; i < len_segment; i++)
{
tmpbuf[i] = buf[processed + i];
}
procFirstSegment(tmpbuf, offset);
for (size_t i = 0; i < len_segment; i++)
{
buf[processed + i] = tmpbuf[i];
}
postProcess(len_segment);
}
// 区块对齐
if (offset % SEGMENT_SIZE != 0) {
size_t len_segment = std::min(SEGMENT_SIZE - (offset % SEGMENT_SIZE), toProcess);
tmpbuf.resize(len_segment);
for (size_t i = 0; i < len_segment; i++)
{
tmpbuf[i] = buf[processed + i];
}
procASegment(tmpbuf, offset);
for (size_t i = 0; i < len_segment; i++)
{
buf[processed + i] = tmpbuf[i];
}
postProcess(len_segment);
}
// 对每个区块逐一进行解密
while (toProcess > SEGMENT_SIZE) {
tmpbuf.resize(SEGMENT_SIZE);
for (size_t i = 0; i < SEGMENT_SIZE; i++)
{
tmpbuf[i] = buf[processed + i];
}
procASegment(tmpbuf, offset);
for (size_t i = 0; i < SEGMENT_SIZE; i++)
{
buf[processed + i] = tmpbuf[i];
}
postProcess(SEGMENT_SIZE);
}
if (toProcess > 0) {
tmpbuf.resize(toProcess);
for (size_t i = 0; i < toProcess; i++)
{
tmpbuf[i] = buf[processed + i];
}
procASegment(tmpbuf, offset);
for (size_t i = 0; i < toProcess; i++)
{
buf[processed + i] = tmpbuf[i];
}
}
#undef postProcess
}
QmcRC4Cipher(std::vector<uint8_t>& argKey, short operation) {
if (operation == 2)
{
if (argKey.size() == 0) {
return;
}
}
else if (operation == 1)
{
const char WordList[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
srand(time(0));
uint32_t number = 0;
while (number <= 300 || number >= 512)
{
number = rand();
}
argKey.resize(number);
for (int i = 0; i < argKey.size(); i++) {
number = rand();
argKey[i] = WordList[number % 62];
}
}
else
{
return;
}
key = argKey;
// init seed box
S.resize(key.size());
for (size_t i = 0; i < key.size(); ++i) {
S[i] = i & 0xff;
}
size_t j = 0;
for (size_t i = 0; i < key.size(); ++i) {
j = (S[i] + j + key[i % key.size()]) % key.size();
std::swap(S[i], S[j]);
}
// init hash base
hash = 1;
for (size_t i = 0; i < key.size(); i++) {
uint8_t value = key[i];
// ignore if key char is '\x00'
if (!value) continue;
auto next_hash = hash * value;
if (next_hash == 0 || next_hash <= hash) break;
hash = next_hash;
}
}
private:
const size_t FIRST_SEGMENT_SIZE = 0x80;
const size_t SEGMENT_SIZE = 5120;
std::vector<uint8_t> S;
std::vector<uint8_t> key;
uint32_t hash = 1;
void procFirstSegment(std::vector<uint8_t>& buf, size_t offset) {
for (size_t i = 0; i < buf.size(); i++) {
buf[i] ^= key[getSegmentKey(offset + i)];
}
}
void procASegment(std::vector<uint8_t>& buf, size_t offset) {
// Initialise a new seed box
std::vector<uint8_t> nS;
nS = S;
// Calculate the number of bytes to skip.
// The initial "key" derived from segment id, plus the current offset.
int64_t skipLen = (offset % SEGMENT_SIZE) + getSegmentKey(int(offset / SEGMENT_SIZE));
// decrypt the block
size_t j = 0;
size_t k = 0;
int i = -skipLen;
for (; i < (int)buf.size(); i++) {
j = (j + 1) % key.size();
k = (nS[j] + k) % key.size();
std::swap(nS[k], nS[j]);
if (i >= 0) {
buf[i] ^= nS[(nS[j] + nS[k]) % key.size()];
}
}
}
uint64_t getSegmentKey(int id) {
auto seed = key[id % key.size()];
uint64_t idx = ((double)hash / ((id + 1) * seed)) * 100.0;
return idx % key.size();
}
};

217
src/QmcWasm/qmc_key.hpp Normal file
View File

@ -0,0 +1,217 @@
#include"TencentTea.hpp"
#include "base64.hpp"
void simpleMakeKey(uint8_t salt, int length, std::vector<uint8_t> &key_buf) {
for (size_t i = 0; i < length; ++i) {
double tmp = tan((float)salt + (double)i * 0.1);
key_buf[i] = 0xFF & (uint8_t)(fabs(tmp) * 100.0);
}
}
std::vector<uint8_t> v2KeyPrefix = { 0x51, 0x51, 0x4D, 0x75, 0x73, 0x69, 0x63, 0x20, 0x45, 0x6E, 0x63, 0x56, 0x32, 0x2C, 0x4B, 0x65, 0x79, 0x3A };
bool decryptV2Key(std::vector<uint8_t> key, std::vector<uint8_t>& outVec)
{
if (v2KeyPrefix.size() > key.size())
{
return true;
}
for (size_t i = 0; i < v2KeyPrefix.size(); i++)
{
if (key[i] != v2KeyPrefix[i])
{
return true;
}
}
std::vector<uint8_t> mixKey1 = { 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 };
std::vector<uint8_t> mixKey2 = { 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 };
std::vector<uint8_t> out;
std::vector<uint8_t> tmpKey;
tmpKey.resize(key.size() - 18);
for (size_t i = 0; i < tmpKey.size(); i++)
{
tmpKey[i] = key[18 + i];
}
if (!decryptTencentTea(tmpKey, mixKey1, out))
{
outVec.resize(0);
//EncV2 key decode failed.
return false;
}
tmpKey.resize(out.size());
for (size_t i = 0; i < tmpKey.size(); i++)
{
tmpKey[i] = out[i];
}
out.resize(0);
if (!decryptTencentTea(tmpKey, mixKey2, out))
{
outVec.resize(0);
//EncV2 key decode failed.
return false;
}
outVec.resize(base64::decoded_size(out.size()));
auto n = base64::decode(outVec.data(), (const char*)(out.data()), out.size()).first;
if (n < 16)
{
outVec.resize(0);
//EncV2 key size is too small.
return false;
}
outVec.resize(n);
return true;
}
bool encryptV2Key(std::vector<uint8_t> key, std::vector<uint8_t>& outVec)
{
if (key.size() < 16)
{
outVec.resize(0);
//EncV2 key size is too small.
return false;
}
std::vector<uint8_t> in;
in.resize(base64::encoded_size(key.size()));
auto n = base64::encode(in.data(), (const char*)(key.data()), key.size());
in.resize(n);
std::vector<uint8_t> mixKey1 = { 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 };
std::vector<uint8_t> mixKey2 = { 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 };
std::vector<uint8_t> tmpKey;
if (!encryptTencentTea(in, mixKey2, tmpKey))
{
outVec.resize(0);
//EncV2 key decode failed.
return false;
}
in.resize(tmpKey.size());
for (size_t i = 0; i < tmpKey.size(); i++)
{
in[i] = tmpKey[i];
}
tmpKey.resize(0);
if (!encryptTencentTea(in, mixKey1, tmpKey))
{
outVec.resize(0);
//EncV2 key decode failed.
return false;
}
outVec.resize(tmpKey.size() + 18);
for (size_t i = 0; i < tmpKey.size(); i++)
{
outVec[18 + i] = tmpKey[i];
}
for (size_t i = 0; i < v2KeyPrefix.size(); i++)
{
outVec[i] = v2KeyPrefix[i];
}
return true;
}
bool QmcDecryptKey(std::vector<uint8_t> raw, std::vector<uint8_t> &outVec) {
std::vector<uint8_t> rawDec;
rawDec.resize(base64::decoded_size(raw.size()));
auto n = base64::decode(rawDec.data(), (const char*)(raw.data()), raw.size()).first;
if (n < 16) {
return false;
//key length is too short
}
rawDec.resize(n);
std::vector<uint8_t> tmpIn = rawDec;
if (!decryptV2Key(tmpIn, rawDec))
{
//decrypt EncV2 failed.
return false;
}
std::vector<uint8_t> simpleKey;
simpleKey.resize(8);
simpleMakeKey(106, 8, simpleKey);
std::vector<uint8_t> teaKey;
teaKey.resize(16);
for (size_t i = 0; i < 8; i++) {
teaKey[i << 1] = simpleKey[i];
teaKey[(i << 1) + 1] = rawDec[i];
}
std::vector<uint8_t> out;
std::vector<uint8_t> tmpRaw;
tmpRaw.resize(rawDec.size() - 8);
for (size_t i = 0; i < tmpRaw.size(); i++)
{
tmpRaw[i] = rawDec[8 + i];
}
if (decryptTencentTea(tmpRaw, teaKey, out))
{
rawDec.resize(8 + out.size());
for (size_t i = 0; i < out.size(); i++)
{
rawDec[8 + i] = out[i];
}
outVec = rawDec;
return true;
}
else
{
return false;
}
}
bool QmcEncryptKey(std::vector<uint8_t> raw, std::vector<uint8_t>& outVec, bool useEncV2 = true) {
std::vector<uint8_t> simpleKey;
simpleKey.resize(8);
simpleMakeKey(106, 8, simpleKey);
std::vector<uint8_t> teaKey;
teaKey.resize(16);
for (size_t i = 0; i < 8; i++) {
teaKey[i << 1] = simpleKey[i];
teaKey[(i << 1) + 1] = raw[i];
}
std::vector<uint8_t> out;
out.resize(raw.size() - 8);
for (size_t i = 0; i < out.size(); i++)
{
out[i] = raw[8 + i];
}
std::vector<uint8_t> tmpRaw;
if (encryptTencentTea(out, teaKey, tmpRaw))
{
raw.resize(tmpRaw.size() + 8);
for (size_t i = 0; i < tmpRaw.size(); i++)
{
raw[i + 8] = tmpRaw[i];
}
if (useEncV2)
{
std::vector<uint8_t> tmpIn = raw;
if (!encryptV2Key(tmpIn, raw))
{
//encrypt EncV2 failed.
return false;
}
}
std::vector<uint8_t> rawEnc;
rawEnc.resize(base64::encoded_size(raw.size()));
auto n = base64::encode(rawEnc.data(), (const char*)(raw.data()), raw.size());
rawEnc.resize(n);
outVec = rawEnc;
return true;
}
else
{
return false;
}
}

View File

@ -0,0 +1,2 @@
// Polyfill for node.
global.Blob = global.Blob || require("node:buffer").Blob;

View File

@ -0,0 +1,113 @@
<style scoped>
label {
cursor: pointer;
line-height: 1.2;
display: block;
}
.item-desc {
color: #aaa;
font-size: small;
display: block;
line-height: 1.2;
margin-top: 0.2em;
}
.item-desc a {
color: #aaa;
}
form >>> input {
font-family: 'Courier New', Courier, monospace;
}
* >>> .um-config-dialog {
max-width: 90%;
width: 40em;
}
</style>
<template>
<el-dialog @close="cancel()" title="解密设定" :visible="show" custom-class="um-config-dialog" center>
<el-form ref="form" :rules="rules" status-icon :model="form" label-width="0">
<section>
<label>
<span>
JOOX Music ·
<Ruby caption="Unique Device Identifier">设备唯一识别码</Ruby>
</span>
<el-form-item prop="jooxUUID">
<el-input type="text" v-model="form.jooxUUID" clearable maxlength="32" show-word-limit> </el-input>
</el-form-item>
</label>
<p class="item-desc">
下载该加密文件的 JOOX 应用所记录的设备唯一识别码
<br />
参见
<a href="https://github.com/unlock-music/joox-crypto/wiki/%E8%8E%B7%E5%8F%96%E8%AE%BE%E5%A4%87-UUID">
获取设备 UUID · unlock-music/joox-crypto Wiki</a
>
</p>
</section>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="primary" :loading="saving" @click="emitConfirm()"> </el-button>
</span>
</el-dialog>
</template>
<script>
import { storage } from '@/utils/storage';
import Ruby from './Ruby';
// FIXME:
function validateJooxUUID(rule, value, callback) {
if (!value || !/^[\da-fA-F]{32}$/.test(value)) {
callback(new Error('无效的 Joox UUID请参考 Wiki 获取。'));
} else {
callback();
}
}
const rules = {
jooxUUID: { validator: validateJooxUUID, trigger: 'change' },
};
export default {
components: {
Ruby,
},
props: {
show: { type: Boolean, required: true },
},
data() {
return {
rules,
saving: false,
form: {
jooxUUID: '',
},
centerDialogVisible: false,
};
},
async mounted() {
await this.resetForm();
},
methods: {
async resetForm() {
this.form.jooxUUID = await storage.loadJooxUUID();
},
async cancel() {
await this.resetForm();
this.$emit('done');
},
async emitConfirm() {
this.saving = true;
await storage.saveJooxUUID(this.form.jooxUUID);
this.saving = false;
this.$emit('done');
},
},
};
</script>

View File

@ -0,0 +1,178 @@
<style scoped>
label {
cursor: pointer;
line-height: 1.2;
display: block;
}
.item-desc {
color: #aaa;
font-size: small;
display: block;
line-height: 1.2;
margin-top: 0.2em;
}
.item-desc a {
color: #aaa;
}
form >>> input {
font-family: 'Courier New', Courier, monospace;
}
* >>> .um-edit-dialog {
max-width: 90%;
width: 30em;
}
</style>
<template>
<el-dialog @close="cancel()" title="音乐标签编辑" :visible="show" custom-class="um-edit-dialog" center>
<el-form ref="form" status-icon :model="form" label-width="0">
<section>
<el-image v-show="!editPicture" :src="imgFile.url || picture" style="width: 100px; height: 100px">
<div slot="error" class="image-slot el-image__error">暂无封面</div>
</el-image>
<el-upload v-show="editPicture" :auto-upload="false" :on-change="addFile" :on-remove="rmvFile" :show-file-list="true" :limit="1" list-type="picture" action="" drag>
<i class="el-icon-upload" />
<div class="el-upload__text">将新图片拖到此处<em>点击选择</em><br />以替换自动匹配的图片</div>
<div slot="tip" class="el-upload__tip">
新拖到此处的图片将覆盖原始图片
</div>
</el-upload>
<i
:class="{'el-icon-edit': !editPicture, 'el-icon-check': editPicture}"
@click="changeCover"
></i><br />
标题:
<span v-show="!editTitle">{{title}}</span>
<el-input v-show="editTitle" v-model="title"></el-input>
<i
:class="{'el-icon-edit': !editTitle, 'el-icon-check': editTitle}"
@click="editTitle = !editTitle"
></i><br />
艺术家:
<span v-show="!editArtist">{{artist}}</span>
<el-input v-show="editArtist" v-model="artist"></el-input>
<i
:class="{'el-icon-edit': !editArtist, 'el-icon-check': editArtist}"
@click="editArtist = !editArtist"
></i><br />
专辑:
<span v-show="!editAlbum">{{album}}</span>
<el-input v-show="editAlbum" v-model="album"></el-input>
<i
:class="{'el-icon-edit': !editAlbum, 'el-icon-check': editAlbum}"
@click="editAlbum = !editAlbum"
></i><br />
专辑艺术家:
<span v-show="!editAlbumartist">{{albumartist}}</span>
<el-input v-show="editAlbumartist" v-model="albumartist"></el-input>
<i
:class="{'el-icon-edit': !editAlbumartist, 'el-icon-check': editAlbumartist}"
@click="editAlbumartist = !editAlbumartist"
></i><br />
风格:
<span v-show="!editGenre">{{genre}}</span>
<el-input v-show="editGenre" v-model="genre"></el-input>
<i
:class="{'el-icon-edit': !editGenre, 'el-icon-check': editGenre}"
@click="editGenre = !editGenre"
></i><br />
<p class="item-desc">
为了节省您设备的资源请在确定前充分检查避免反复修改<br />
直接关闭此对话框不会保留所作的更改
</p>
</section>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="emitConfirm()"> </el-button>
</span>
</el-dialog>
</template>
<script>
import Ruby from './Ruby';
export default {
components: {
Ruby,
},
props: {
show: { type: Boolean, required: true },
picture: { type: String | undefined, required: true },
title: { type: String | undefined, required: true },
artist: { type: String | undefined, required: true },
album: { type: String | undefined, required: true },
albumartist: { type: String | undefined, required: true },
genre: { type: String | undefined, required: true },
},
data() {
return {
form: {
},
imgFile: { tmpblob: undefined, blob: undefined, url: undefined },
editPicture: false,
editTitle: false,
editArtist: false,
editAlbum: false,
editAlbumartist: false,
editGenre: false,
};
},
async mounted() {
this.refreshForm();
},
methods: {
addFile(file) {
this.imgFile.tmpblob = file.raw;
},
rmvFile() {
this.imgFile.tmpblob = undefined;
},
changeCover() {
this.editPicture = !this.editPicture;
if (!this.editPicture && this.imgFile.tmpblob) {
this.imgFile.blob = this.imgFile.tmpblob;
if (this.imgFile.url) {
URL.revokeObjectURL(this.imgFile.url);
}
this.imgFile.url = URL.createObjectURL(this.imgFile.blob);
}
},
async refreshForm() {
if (this.imgFile.url) {
URL.revokeObjectURL(this.imgFile.url);
}
this.imgFile = { tmpblob: undefined, blob: undefined, url: undefined };
this.editPicture = false;
this.editTitle = false;
this.editArtist = false;
this.editAlbum = false;
this.editAlbumartist = false;
this.editGenre = false;
},
async cancel() {
this.refreshForm();
this.$emit('cancel');
},
async emitConfirm() {
if (this.editPicture) {
this.changeCover();
}
if (this.imgFile.url) {
URL.revokeObjectURL(this.imgFile.url);
}
this.$emit('ok', {
picture: this.imgFile.blob,
title: this.title,
artist: this.artist,
album: this.album,
albumartist: this.albumartist,
genre: this.genre,
});
},
},
};
</script>

View File

@ -0,0 +1,91 @@
<template>
<el-upload :auto-upload="false" :on-change="addFile" :show-file-list="false" action="" drag multiple>
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处 <em>点击选择</em></div>
<div slot="tip" class="el-upload__tip">
<div>
仅在浏览器内对文件进行解锁无需消耗流量
<el-tooltip effect="dark" placement="top-start">
<div slot="content">算法在源代码中已经提供所有运算都发生在本地</div>
<i class="el-icon-info" style="font-size: 12px" />
</el-tooltip>
</div>
<div>
工作模式: {{ parallel ? '多线程 Worker' : '单线程 Queue' }}
<el-tooltip effect="dark" placement="top-start">
<div slot="content">
将此工具部署在HTTPS环境下可以启用Web Worker特性<br />
从而更快的利用并行处理完成解锁
</div>
<i class="el-icon-info" style="font-size: 12px" />
</el-tooltip>
</div>
</div>
<transition name="el-fade-in"
><!--todo: add delay to animation-->
<el-progress
v-show="progress_show"
:format="progress_string"
:percentage="progress_value"
:stroke-width="16"
:text-inside="true"
style="margin: 16px 6px 0 6px"
></el-progress>
</transition>
</el-upload>
</template>
<script>
import { spawn, Worker, Pool } from 'threads';
import { Decrypt } from '@/decrypt';
import { DecryptQueue } from '@/utils/utils';
import { storage } from '@/utils/storage';
export default {
name: 'FileSelector',
data() {
return {
task_all: 0,
task_finished: 0,
queue: new DecryptQueue(), // for http or file protocol
parallel: false,
};
},
computed: {
progress_value() {
return this.task_all ? (this.task_finished / this.task_all) * 100 : 0;
},
progress_show() {
return this.task_all !== this.task_finished;
},
},
mounted() {
if (window.Worker && window.location.protocol !== 'file:' && process.env.NODE_ENV === 'production') {
console.log('Using Worker Pool');
this.queue = Pool(() => spawn(new Worker('@/utils/worker.ts')), navigator.hardwareConcurrency || 1);
this.parallel = true;
} else {
console.log('Using Queue in Main Thread');
}
},
methods: {
progress_string() {
return `${this.task_finished} / ${this.task_all}`;
},
async addFile(file) {
this.task_all++;
this.queue.queue(async (dec = Decrypt) => {
console.log('start handling', file.name);
try {
this.$emit('success', await dec(file, await storage.getAll()));
} catch (e) {
console.error(e);
this.$emit('error', e, file.name);
} finally {
this.task_finished++;
}
});
},
},
};
</script>

View File

@ -0,0 +1,66 @@
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column label="封面">
<template slot-scope="scope">
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
<div slot="error" class="image-slot el-image__error">暂无封面</div>
</el-image>
</template>
</el-table-column>
<el-table-column label="歌曲">
<template #default="scope">
<span>{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column label="歌手">
<template #default="scope">
<p>{{ scope.row.artist }}</p>
</template>
</el-table-column>
<el-table-column label="专辑">
<template #default="scope">
<p>{{ scope.row.album }}</p>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
</el-button>
<el-button circle icon="el-icon-download" @click="handleDownload(scope.row)"></el-button>
<el-button circle icon="el-icon-edit" @click="handleEdit(scope.row)"></el-button>
<el-button circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
import { RemoveBlobMusic } from '@/utils/utils';
export default {
name: 'PreviewTable',
props: {
tableData: { type: Array, required: true },
policy: { type: Number, required: true },
},
methods: {
handlePlay(index, row) {
this.$emit('play', row.file);
},
handleDelete(index, row) {
RemoveBlobMusic(row);
this.tableData.splice(index, 1);
},
handleDownload(row) {
this.$emit('download', row);
},
handleEdit(row) {
this.$emit('edit', row);
},
},
};
</script>
<style scoped></style>

18
src/component/Ruby.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<ruby :title="caption">
<slot></slot>
<rp></rp>
<rt v-text="caption"></rt>
<rp></rp>
</ruby>
</template>
<script>
export default {
name: 'Ruby',
props: {
caption: { type: String, required: true },
},
};
</script>

View File

@ -1,71 +0,0 @@
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column label="封面">
<template slot-scope="scope">
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
<div class="image-slot el-image__error" slot="error">
暂无封面
</div>
</el-image>
</template>
</el-table-column>
<el-table-column label="歌曲">
<template slot-scope="scope">
<span>{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column label="歌手">
<template slot-scope="scope">
<p>{{ scope.row.artist }}</p>
</template>
</el-table-column>
<el-table-column label="专辑">
<template slot-scope="scope">
<p>{{ scope.row.album }}</p>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button @click="handlePlay(scope.$index, scope.row)"
circle icon="el-icon-video-play" type="success">
</el-button>
<el-button @click="handleDownload(scope.row)"
circle icon="el-icon-download">
</el-button>
<el-button @click="handleDelete(scope.$index, scope.row)"
circle icon="el-icon-delete" type="danger">
</el-button>
</template>
</el-table-column>
</el-table>
</template>
<script>
import {DownloadBlobMusic, RemoveBlobMusic} from './util'
export default {
name: "preview",
props: {
tableData: {type: Array, required: true},
download_format: {type: String, required: true}
},
methods: {
handlePlay(index, row) {
this.$emit("music_changed", row.file);
},
handleDelete(index, row) {
RemoveBlobMusic(row);
this.tableData.splice(index, 1);
},
handleDownload(row) {
DownloadBlobMusic(row, this.download_format)
},
}
}
</script>
<style scoped>
</style>

View File

@ -1,120 +0,0 @@
<template>
<el-upload
:auto-upload="false"
:on-change="handleFile"
:show-file-list="false"
action=""
drag
multiple>
<i class="el-icon-upload"/>
<div class="el-upload__text">将文件拖到此处<em>点击选择</em></div>
<div class="el-upload__tip" slot="tip">本工具仅在浏览器内对文件进行解锁无需消耗流量</div>
<transition name="el-fade-in">
<el-progress
:format="progressFormat" :percentage="progress_percent" :stroke-width="16"
:text-inside="true" style="margin: 16px 6px 0 6px"
v-show="progress_show"
></el-progress>
</transition>
</el-upload>
</template>
<script>
"use strict";//
export default {
name: "upload",
data() {
return {
cacheQueue: [],
workers: [],
idle_workers: [],
thread_num: 1,
progress_show: false,
progress_finished: 0,
progress_all: 0,
progress_percent: 0,
}
},
mounted() {
if (document.location.host !== "" && process.env.NODE_ENV === 'production') {
this.thread_num = navigator.hardwareConcurrency || 1;
const worker = require("workerize-loader!../decrypt/common");
// noinspection JSValidateTypes,JSUnresolvedVariable
this.workers.push(worker().CommonDecrypt);
this.idle_workers.push(0);
// delay to optimize for first loading
setTimeout(() => {
for (let i = 1; i < this.thread_num; i++) {
// noinspection JSValidateTypes,JSUnresolvedVariable
this.workers.push(worker().CommonDecrypt);
this.idle_workers.push(i);
}
}, 5000);
} else {
const dec = require('../decrypt/common');
this.workers.push(dec.CommonDecrypt);
this.idle_workers.push(0)
}
},
methods: {
progressFormat() {
return this.progress_finished + "/" + (this.progress_all)
},
progressChange(finish, all) {
this.progress_all += all;
this.progress_finished += finish;
this.progress_percent = Math.round(this.progress_finished / this.progress_all * 100);
if (this.progress_finished === this.progress_all) {
setTimeout(() => {
this.progress_show = false;
this.progress_finished = 0;
this.progress_all = 0;
}, 3000);
} else {
this.progress_show = true;
}
},
handleFile(file) {
this.progressChange(0, +1);
// worker
if (this.idle_workers.length > 0) {
this.handleDoFile(file, this.idle_workers.shift());
}
// worker
else {
this.cacheQueue.push(file);
}
},
handleCacheQueue(worker_id) {
//
if (this.cacheQueue.length === 0) {
this.idle_workers.push(worker_id);
return
}
this.handleDoFile(this.cacheQueue.shift(), worker_id);
},
handleDoFile(file, worker_id) {
this.workers[worker_id](file).then(data => {
this.$emit("handle_finish", data);
// todo: call stack
this.handleCacheQueue(worker_id);
this.progressChange(+1, 0);
}).catch(err => {
this.$emit("handle_error", err, file.name);
this.handleCacheQueue(worker_id);
this.progressChange(+1, 0);
})
},
}
}
</script>
<style scoped>
/*noinspection CssUnusedSymbol*/
.el-upload-dragger {
width: 80vw !important;
}
</style>

View File

@ -1,30 +0,0 @@
export function DownloadBlobMusic(data, format) {
const a = document.createElement('a');
a.href = data.file;
switch (format) {
default:
case "1":
a.download = data.artist + " - " + data.title + "." + data.ext;
break;
case "2":
a.download = data.title + "." + data.ext;
break;
case "3":
a.download = data.title + " - " + data.artist + "." + data.ext;
break;
case "4":
a.download = data.rawFilename + "." + data.ext;
break;
}
document.body.append(a);
a.click();
a.remove();
}
export function RemoveBlobMusic(data) {
URL.revokeObjectURL(data.file);
if (data.picture.startsWith("blob:")) {
URL.revokeObjectURL(data.picture);
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,52 @@
import fs from 'fs';
import { storage } from '@/utils/storage';
import { Decrypt as decryptJoox } from '../joox';
import { extractQQMusicMeta as extractQQMusicMetaOrig } from '@/utils/qm_meta';
jest.mock('@/utils/storage');
jest.mock('@/utils/qm_meta');
const loadJooxUUID = storage.loadJooxUUID as jest.MockedFunction<typeof storage.loadJooxUUID>;
const extractQQMusicMeta = extractQQMusicMetaOrig as jest.MockedFunction<typeof extractQQMusicMetaOrig>;
const TEST_UUID_ZEROS = ''.padStart(32, '0');
const encryptedFile1 = fs.readFileSync(__dirname + '/fixture/joox_1.bin');
describe('decrypt/joox', () => {
it('should be able to decrypt sample file (v4)', async () => {
loadJooxUUID.mockResolvedValue(TEST_UUID_ZEROS);
extractQQMusicMeta.mockImplementationOnce(async (blob: Blob) => {
return {
title: 'unused',
album: 'unused',
blob: blob,
artist: 'unused',
imgUrl: 'https://github.com/unlock-music',
};
});
const result = await decryptJoox(new Blob([encryptedFile1]), 'test.bin', 'bin');
const resultBuf = await result.blob.arrayBuffer();
expect(resultBuf).toEqual(Buffer.from('Hello World', 'utf-8').buffer);
});
it('should reject E!99 files', async () => {
loadJooxUUID.mockResolvedValue(TEST_UUID_ZEROS);
const input = new Blob([Buffer.from('E!99....')]);
await expect(decryptJoox(input, 'test.bin', 'bin')).rejects.toThrow('不支持的 joox 加密格式');
});
it('should reject empty uuid', async () => {
loadJooxUUID.mockResolvedValue('');
const input = new Blob([encryptedFile1]);
await expect(decryptJoox(input, 'test.bin', 'bin')).rejects.toThrow('UUID');
});
it('should reject invalid uuid', async () => {
loadJooxUUID.mockResolvedValue('hello!');
const input = new Blob([encryptedFile1]);
await expect(decryptJoox(input, 'test.bin', 'bin')).rejects.toThrow('UUID');
});
});

View File

@ -1,80 +0,0 @@
import {Decrypt as NcmDecrypt} from "./ncm";
import {Decrypt as XmDecrypt} from "./xm";
import {Decrypt as QmcDecrypt} from "./qmc";
import {Decrypt as KgmDecrypt} from "./kgm";
import {Decrypt as KwmDecrypt} from "@/decrypt/kwm";
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
import {Decrypt as TmDecrypt} from "@/decrypt/tm";
import {DecryptResult} from "@/decrypt/entity";
interface FileInfo {
status: string
name: string,
size: number,
percentage: number,
uid: number,
raw: File
}
export async function CommonDecrypt(file: FileInfo) {
let raw_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase();
let raw_filename = file.name.substring(0, file.name.lastIndexOf("."));
let rt_data: Partial<DecryptResult>;
switch (raw_ext) {
case "ncm":// Netease Mp3/Flac
rt_data = await NcmDecrypt(file.raw, raw_filename, raw_ext);
break;
case "kwm":// Kuwo Mp3/Flac
rt_data = await KwmDecrypt(file.raw, raw_filename, 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_filename, raw_ext);
break;
case "ogg":// Raw Ogg
rt_data = await RawDecrypt(file.raw, raw_filename, raw_ext);
break;
case "tm0":// QQ Music IOS Mp3
case "tm3":// QQ Music IOS Mp3
rt_data = await RawDecrypt(file.raw, raw_filename, "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 Desktop Flac
case "mgg": //QQ Music Desktop 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_filename, raw_ext);
break;
case "tm2":// QQ Music IOS M4a
case "tm6":// QQ Music IOS M4a
rt_data = await TmDecrypt(file.raw, raw_filename);
break;
case "vpr":
case "kgm":
case "kgma":
rt_data = await KgmDecrypt(file.raw, raw_filename, raw_ext);
break
default:
rt_data = {status: false, message: "不支持此文件格式",}
}
if (!rt_data.rawExt) rt_data.rawExt = raw_ext;
if (!rt_data.rawFilename) rt_data.rawFilename = raw_filename;
console.log(rt_data);
return rt_data;
}

View File

@ -1,18 +1,25 @@
export interface DecryptResult {
status: boolean,//todo: remove & use Exception
title: string;
album?: string;
artist?: string;
title: string
album?: string
artist?: string
mime: string;
ext: string;
mime: string
ext: string
file: string
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;
}

115
src/decrypt/index.ts Normal file
View File

@ -0,0 +1,115 @@
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';
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 { Decrypt as JooxDecrypt } from '@/decrypt/joox';
import { Decrypt as XimalayaDecrypt } from './ximalaya';
import { DecryptResult, FileInfo } from '@/decrypt/entity';
import { SplitFilename } from '@/decrypt/utils';
import { storage } from '@/utils/storage';
import InMemoryStorage from '@/utils/storage/InMemoryStorage';
export async function Decrypt(file: FileInfo, config: Record<string, any>): Promise<DecryptResult> {
// Worker thread will fallback to in-memory storage.
if (storage instanceof InMemoryStorage) {
await storage.setAll(config);
}
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;
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 'qmc0': //QQ Music Android Mp3
case 'qmc3': //QQ Music Android Mp3
case 'qmc2': //QQ Music Android Ogg
case 'qmc4': //QQ Music Android Ogg
case 'qmc6': //QQ Music Android Ogg
case 'qmc8': //QQ Music Android Ogg
case 'qmcflac': //QQ Music Android Flac
case 'qmcogg': //QQ Music Android Ogg
case 'tkm': //QQ Music Accompaniment M4a
// Moo Music
case 'bkcmp3':
case 'bkcm4a':
case 'bkcflac':
case 'bkcwav':
case 'bkcape':
case 'bkcogg':
case 'bkcwma':
// QQ Music v2
case 'mggl': //QQ Music Mac
case 'mflac': //QQ Music New Flac
case 'mflac0': //QQ Music New Flac
case 'mflach': //QQ Music New Flac
case 'mgg': //QQ Music New Ogg
case 'mgg1': //QQ Music New Ogg
case 'mgg0':
case 'mmp4': // QMC MP4 Container w/ E-AC-3 JOC
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;
case 'ofl_en':
rt_data = await JooxDecrypt(file.raw, raw.name, raw.ext);
break;
case 'x2m':
case 'x3m':
rt_data = await XimalayaDecrypt(file.raw, raw.name, raw.ext);
break;
case 'mflach': //QQ Music New Flac
throw '网页版无法解锁,请使用<a target="_blank" href="https://git.unlock-music.dev/um/cli">CLI版本</a>'
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;
}

44
src/decrypt/joox.ts Normal file
View File

@ -0,0 +1,44 @@
import jooxFactory from '@unlock-music/joox-crypto';
import { DecryptResult } from './entity';
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from './utils';
import { MergeUint8Array } from '@/utils/MergeUint8Array';
import { storage } from '@/utils/storage';
import { extractQQMusicMeta } from '@/utils/qm_meta';
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
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 songId = raw_filename.match(/^(\d+)\s\[mqms\d*]$/i)?.[1];
const { album, artist, imgUrl, blob, title } = await extractQQMusicMeta(
new Blob([musicDecoded], { type: mime }),
raw_filename,
ext,
songId,
);
return {
title: title,
artist: artist,
ext: ext,
album: album,
picture: imgUrl,
file: URL.createObjectURL(blob),
blob: blob,
mime: mime,
};
}

View File

@ -1,126 +0,0 @@
import {
AudioMimeType,
BytesHasPrefix,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt
} from "@/decrypt/utils.ts";
import {parseBlob as metaParseBlob} from "music-metadata-browser";
const VprHeader = [
0x05, 0x28, 0xBC, 0x96, 0xE9, 0xE4, 0x5A, 0x43,
0x91, 0xAA, 0xBD, 0xD0, 0x7A, 0xF5, 0x36, 0x31]
const KgmHeader = [
0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B,
0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14]
const VprMaskDiff = [0x25, 0xDF, 0xE8, 0xA6, 0x75, 0x1E, 0x75, 0x0E,
0x2F, 0x80, 0xF3, 0x2D, 0xB8, 0xB6, 0xE3, 0x11,
0x00]
export async function Decrypt(file, raw_filename, raw_ext) {
try {
if (window.location.protocol === "file:") {
return {
status: false,
message: "请使用<a target='_blank' href='https://github.com/ix64/unlock-music/wiki/其他音乐格式工具'>CLI版本</a>进行解锁"
}
}
} catch {
}
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (raw_ext === "vpr") {
if (!BytesHasPrefix(oriData, VprHeader))
return {status: false, message: "Not a valid vpr file!"}
} else {
if (!BytesHasPrefix(oriData, KgmHeader))
return {status: false, message: "Not a valid kgm/kgma 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) {
return {
status: false,
message: "文件过大,请使用<a target='_blank' href='https://github.com/ix64/unlock-music/wiki/其他音乐格式工具'>CLI版本</a>进行解锁"
}
}
let key1 = new Uint8Array(17)
key1.set(oriData.slice(0x1c, 0x2c), 0)
if (MaskV2 == null) {
if (!await LoadMaskV2()) {
return {status: false, message: "加载Kgm/Vpr Mask数据失败"}
}
}
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]
}
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 {
status: true,
album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta),
file: URL.createObjectURL(musicBlob),
ext,
mime,
title,
artist
}
}
function GetMask(pos) {
return MaskV2PreDef[pos % 272] ^ MaskV2[pos >> 4]
}
let MaskV2 = null;
async function LoadMaskV2() {
try {
let resp = await fetch("./static/kgm.mask", {
method: "GET"
})
MaskV2 = new Uint8Array(await resp.arrayBuffer());
return true
} catch (e) {
console.error(e)
return false
}
}
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,
]

77
src/decrypt/kgm.ts Normal file
View File

@ -0,0 +1,77 @@
import {
AudioMimeType,
BytesHasPrefix,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt,
} from '@/decrypt/utils';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
import { DecryptResult } from '@/decrypt/entity';
import { DecryptKgmWasm } from '@/decrypt/kgm_wasm';
import { decryptKgmByteAtOffsetV2, decryptVprByteAtOffset } from '@jixun/kugou-crypto/dist/utils/decryptionHelper';
//prettier-ignore
const VprHeader = [
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
]
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const oriData = await GetArrayBuffer(file);
if (raw_ext === 'vpr') {
if (!BytesHasPrefix(new Uint8Array(oriData), VprHeader)) throw Error('Not a valid vpr file!');
} else {
if (!BytesHasPrefix(new Uint8Array(oriData), KgmHeader)) throw Error('Not a valid kgm(a) file!');
}
let musicDecoded: Uint8Array | undefined;
if (globalThis.WebAssembly) {
console.log('kgm: using wasm decoder');
const kgmDecrypted = await DecryptKgmWasm(oriData, raw_ext);
if (kgmDecrypted.success) {
musicDecoded = kgmDecrypted.data;
console.log('kgm wasm decoder suceeded');
} else {
console.warn('KgmWasm failed with error %s', kgmDecrypted.error || '(unknown error)');
}
}
if (!musicDecoded) {
musicDecoded = new Uint8Array(oriData);
let bHeaderLen = new DataView(musicDecoded.slice(0x10, 0x14).buffer);
let headerLen = bHeaderLen.getUint32(0, true);
let key1 = Array.from(musicDecoded.slice(0x1c, 0x2c));
key1.push(0);
musicDecoded = musicDecoded.slice(headerLen);
let dataLen = musicDecoded.length;
const decryptByte = raw_ext === 'vpr' ? decryptVprByteAtOffset : decryptKgmByteAtOffsetV2;
for (let i = 0; i < dataLen; i++) {
musicDecoded[i] = decryptByte(musicDecoded[i], key1, i);
}
}
const ext = SniffAudioExt(musicDecoded);
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, String(musicMeta.common.artists || musicMeta.common.artist || ""));
return {
album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta),
file: URL.createObjectURL(musicBlob),
blob: musicBlob,
ext,
mime,
title,
artist,
};
}

67
src/decrypt/kgm_wasm.ts Normal file
View File

@ -0,0 +1,67 @@
import KgmCryptoModule from '@/KgmWasm/KgmWasmBundle';
import { MergeUint8Array } from '@/utils/MergeUint8Array';
// 每次处理 2M 的数据
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
export interface KGMDecryptionResult {
success: boolean;
data: Uint8Array;
error: string;
}
/**
* KGM
*
* Uint8Array
* @param {ArrayBuffer} kgmBlob Blob
*/
export async function DecryptKgmWasm(kgmBlob: ArrayBuffer, ext: string): Promise<KGMDecryptionResult> {
const result: KGMDecryptionResult = { success: false, data: new Uint8Array(), error: '' };
// 初始化模组
let KgmCrypto: any;
try {
KgmCrypto = await KgmCryptoModule();
} catch (err: any) {
result.error = err?.message || 'wasm 加载失败';
return result;
}
if (!KgmCrypto) {
result.error = 'wasm 加载失败';
return result;
}
// 申请内存块,并文件末端数据到 WASM 的内存堆
let kgmBuf = new Uint8Array(kgmBlob);
const pQmcBuf = KgmCrypto._malloc(DECRYPTION_BUF_SIZE);
KgmCrypto.writeArrayToMemory(kgmBuf.slice(0, DECRYPTION_BUF_SIZE), pQmcBuf);
// 进行解密初始化
const headerSize = KgmCrypto.preDec(pQmcBuf, DECRYPTION_BUF_SIZE, ext);
console.log(headerSize);
kgmBuf = kgmBuf.slice(headerSize);
const decryptedParts = [];
let offset = 0;
let bytesToDecrypt = kgmBuf.length;
while (bytesToDecrypt > 0) {
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
// 解密一些片段
const blockData = new Uint8Array(kgmBuf.slice(offset, offset + blockSize));
KgmCrypto.writeArrayToMemory(blockData, pQmcBuf);
KgmCrypto.decBlob(pQmcBuf, blockSize, offset);
decryptedParts.push(KgmCrypto.HEAPU8.slice(pQmcBuf, pQmcBuf + blockSize));
offset += blockSize;
bytesToDecrypt -= blockSize;
}
KgmCrypto._free(pQmcBuf);
result.data = MergeUint8Array(decryptedParts);
result.success = true;
return result;
}

View File

@ -1,77 +1,78 @@
import {
AudioMimeType,
BytesHasPrefix,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt
} from "@/decrypt/utils.ts";
import {Decrypt as RawDecrypt} from "@/decrypt/raw.ts";
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,
]
const PreDefinedKey = "MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk"
];
const MagicHeader2 = [
0x79, 0x65, 0x65, 0x6C, 0x69, 0x6F, 0x6E, 0x2D,
0x6B, 0x75, 0x77, 0x6F, 0x00, 0x00, 0x00, 0x00,
];
const PreDefinedKey = 'MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk';
export async function Decrypt(file: File, raw_filename: string, _: string): Promise<DecryptResult> {
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) && !BytesHasPrefix(oriData, MagicHeader2)) {
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 {
status: true,
album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta),
file: URL.createObjectURL(musicBlob),
mime,
title,
artist,
ext
}
const musicMeta = await metaParseBlob(musicBlob);
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || 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;
}

71
src/decrypt/mg3d.ts Normal file
View File

@ -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<DecryptResult> {
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);
}

View File

@ -1,175 +0,0 @@
import {AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils.ts";
import {parseBlob as metaParseBlob} from "music-metadata-browser";
const CryptoJS = require("crypto-js");
const MetaFlac = require('metaflac-js');
const CORE_KEY = CryptoJS.enc.Hex.parse("687a4852416d736f356b496e62617857");
const META_KEY = CryptoJS.enc.Hex.parse("2331346C6A6B5F215C5D2630553C2728");
const MagicHeader = [0x43, 0x54, 0x45, 0x4E, 0x46, 0x44, 0x41, 0x4D];
import jimp from 'jimp';
import {
GetWebImage,
WriteMp3Meta
} from "./util"
export async function Decrypt(file, raw_filename, _) {
const fileBuffer = await GetArrayBuffer(file);
const dataView = new DataView(fileBuffer);
if (!BytesHasPrefix(new Uint8Array(fileBuffer, 0, 8), MagicHeader))
return {status: false, message: "此ncm文件已损坏"};
const keyDataObj = getKeyData(dataView, fileBuffer, 10);
const keyBox = getKeyBox(keyDataObj.data);
const musicMetaObj = getMetaData(dataView, fileBuffer, keyDataObj.offset);
const musicMeta = musicMetaObj.data;
let audioOffset = musicMetaObj.offset + dataView.getUint32(musicMetaObj.offset + 5, true) + 13;
let audioData = new Uint8Array(fileBuffer, audioOffset);
let lenAudioData = audioData.length;
for (let cur = 0; cur < lenAudioData; ++cur) audioData[cur] ^= keyBox[cur & 0xff];
if (musicMeta.album === undefined) musicMeta.album = "";
const artists = [];
if (!!musicMeta.artist) musicMeta.artist.forEach(arr => artists.push(arr[0]));
const info = GetMetaFromFile(raw_filename, musicMeta.musicName, artists.join("; "))
if (artists.length === 0) artists.push(info.artist);
if (musicMeta.format === undefined) musicMeta.format = SniffAudioExt(audioData);
const imageInfo = await GetWebImage(musicMeta.albumPic);
while (!!imageInfo.buffer && imageInfo.buffer.byteLength >= 16 * 1024 * 1024) {
let img = await jimp.read(imageInfo.buffer)
await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO)
imageInfo.buffer = await img.getBufferAsync("image/jpeg")
}
const mime = AudioMimeType[musicMeta.format]
try {
let musicBlob = new Blob([audioData], {type: mime});
const originalMeta = await metaParseBlob(musicBlob);
let shouldWrite = !originalMeta.common.album && !originalMeta.common.artists && !originalMeta.common.title
if (musicMeta.format === "mp3") {
audioData = await WriteMp3Meta(
audioData, artists, info.title, musicMeta.album, imageInfo.buffer, musicMeta.albumPic, shouldWrite ? null : originalMeta)
} else if (musicMeta.format === "flac") {
const writer = new MetaFlac(Buffer.from(audioData))
if (shouldWrite) {
writer.setTag("TITLE=" + info.title)
writer.setTag("ALBUM=" + musicMeta.album)
writer.removeTag("ARTIST")
artists.forEach(artist => writer.setTag("ARTIST=" + artist))
}
writer.importPictureFromBuffer(Buffer.from(imageInfo.buffer))
audioData = writer.save()
}
} catch (e) {
console.warn("Error while appending cover image to file " + e)
}
const musicData = new Blob([audioData], {type: mime})
return {
status: true,
title: info.title,
artist: info.artist,
ext: musicMeta.format,
album: musicMeta.album,
picture: imageInfo.url,
file: URL.createObjectURL(musicData),
mime: mime
}
}
function getKeyData(dataView, fileBuffer, offset) {
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 {offset: offset, data: result.slice(17)};
}
function getKeyBox(keyData) {
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
*/
function getMetaData(dataView, fileBuffer, offset) {
const metaDataLen = dataView.getUint32(offset, true);
offset += 4;
if (metaDataLen === 0) return {data: {}, offset: offset};
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}
).toString(CryptoJS.enc.Utf8);
const labelIndex = plainText.indexOf(":");
let result = JSON.parse(plainText.slice(labelIndex + 1));
if (plainText.slice(0, labelIndex) === "dj") {
result = result.mainMusic;
}
if (!!result.albumPic && result.albumPic !== "")
result.albumPic = result.albumPic.replace("http://", "https://") + "?param=500y500";
return {data: result, offset: offset};
}

237
src/decrypt/ncm.ts Normal file
View File

@ -0,0 +1,237 @@
import {
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 { 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<DecryptResult> {
return new NcmDecrypt(await GetArrayBuffer(file), raw_filename).decrypt();
}
interface NcmMusicMeta {
//musicId: number
musicName?: string;
artist?: Array<string | number>[];
format?: string;
album?: string;
albumPic?: string;
}
interface NcmDjMeta {
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 };
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;
}
return result.slice(17);
}
_getKeyBox(): Uint8Array {
const keyData = this._getKeyData();
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];
});
}
_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;
}
_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(<string>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 _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();
}
}

28
src/decrypt/ncmcache.ts Normal file
View File

@ -0,0 +1,28 @@
import { AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
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, String(tag.common.artists || tag.common.artist || ""));
return {
title,
artist,
ext,
album: tag.common.album,
picture: GetCoverFromFile(tag),
file: URL.createObjectURL(file),
blob: file,
mime: AudioMimeType[ext],
};
}

View File

@ -1,156 +0,0 @@
import {
GetWebImage,
IXAREA_API_ENDPOINT,
WriteMp3Meta
} from "./util";
import {QmcMaskCreate58, QmcMaskDetectMflac, QmcMaskDetectMgg, QmcMaskGetDefault} from "./qmcMask";
import {fromByteArray as Base64Encode, toByteArray as Base64Decode} from 'base64-js'
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils.ts";
import {parseBlob as metaParseBlob} from "music-metadata-browser";
const MetaFlac = require('metaflac-js');
const iconv = require('iconv-lite');
const decode = iconv.decode
const HandlerMap = {
"mgg": {handler: QmcMaskDetectMgg, ext: "ogg", detect: true},
"mflac": {handler: QmcMaskDetectMflac, ext: "flac", detect: true},
"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}
};
export async function Decrypt(file, raw_filename, raw_ext) {
if (!(raw_ext in HandlerMap)) return {status: false, message: "File type is incorrect!"};
const handler = HandlerMap[raw_ext];
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 === undefined) seed = await queryKeyInfo(keyData, raw_filename, raw_ext);
if (seed === undefined) return {status: false, message: raw_ext + "格式仅提供实验性支持"};
} else {
audioData = fileData;
seed = handler.handler(audioData);
}
let musicDecoded = seed.Decrypt(audioData);
const ext = SniffAudioExt(musicDecoded, handler.ext);
const mime = AudioMimeType[ext];
let musicBlob = new Blob([musicDecoded], {type: mime});
const musicMeta = await metaParseBlob(musicBlob);
for (let metaIdx in musicMeta.native) {
if (musicMeta.native[metaIdx].some(item => item.id === "TCON" && item.value === "(12)")) {
console.warn("The metadata is using gbk encoding")
musicMeta.common.artist = decode(musicMeta.common.artist, "gbk");
musicMeta.common.title = decode(musicMeta.common.title, "gbk");
musicMeta.common.album = decode(musicMeta.common.album, "gbk");
}
}
const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist)
if (handler.detect) reportKeyUsage(keyData, seed.Matrix128,
info.artist, info.title, musicMeta.common.album, raw_filename, raw_ext);
let imgUrl = GetCoverFromFile(musicMeta);
if (!imgUrl) {
imgUrl = await queryAlbumCoverImage(info.artist, info.title, musicMeta.common.album);
if (imgUrl !== "") {
const imageInfo = await GetWebImage(imgUrl);
if (imageInfo.url !== "") {
imgUrl = imageInfo.url
try {
if (ext === "mp3") {
musicDecoded = await WriteMp3Meta(musicDecoded,
info.artist.split(" _ "), info.title, "",
imageInfo.buffer, "Cover", musicMeta)
musicBlob = new Blob([musicDecoded], {type: mime});
} else if (ext === 'flac') {
const writer = new MetaFlac(Buffer.from(musicDecoded))
writer.importPictureFromBuffer(Buffer.from(imageInfo.buffer))
musicDecoded = writer.save()
musicBlob = new Blob([musicDecoded], {type: mime});
} else {
console.info("writing metadata for " + ext + " is not being supported for now")
}
} catch (e) {
console.warn("Error while appending cover image to file " + e)
}
}
}
}
return {
status: true,
title: info.title,
artist: info.artist,
ext: ext,
album: musicMeta.common.album,
picture: imgUrl,
file: URL.createObjectURL(musicBlob),
mime: mime
}
}
function reportKeyUsage(keyData, maskData, artist, title, album, filename, format) {
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
}),
}).then().catch()
}
async function queryKeyInfo(keyData, filename, format) {
try {
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}),
});
let data = await resp.json();
return QmcMaskCreate58(Base64Decode(data.Matrix44));
} catch (e) {
console.log(e);
}
}
async function queryAlbumCoverImage(artist, title, album) {
const song_query_url = IXAREA_API_ENDPOINT + "/music/qq-cover"
try {
const params = {Artist: artist, Title: title, Album: album};
let _url = song_query_url + "?";
for (let pKey in params) {
_url += pKey + "=" + encodeURIComponent(params[pKey]) + "&"
}
const resp = await fetch(_url)
if (resp.ok) {
let data = await resp.json();
return song_query_url + "/" + data.Type + "/" + data.Id
}
} catch (e) {
console.log(e);
}
return "";
}

29
src/decrypt/qmc.test.ts Normal file
View File

@ -0,0 +1,29 @@
import fs from 'fs';
import { QmcDecoder } from '@/decrypt/qmc';
import { BytesEqual } from '@/decrypt/utils';
function loadTestDataDecoder(name: string): {
cipherText: Uint8Array;
clearText: Uint8Array;
} {
const cipherBody = fs.readFileSync(`./testdata/${name}_raw.bin`);
const cipherSuffix = fs.readFileSync(`./testdata/${name}_suffix.bin`);
const cipherText = new Uint8Array(cipherBody.length + cipherSuffix.length);
cipherText.set(cipherBody);
cipherText.set(cipherSuffix, cipherBody.length);
return {
cipherText,
clearText: fs.readFileSync(`testdata/${name}_target.bin`),
};
}
test('qmc: real file', async () => {
const cases = ['mflac0_rc4', 'mflac_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();
expect(BytesEqual(buf, clearText)).toBeTruthy();
}
});

185
src/decrypt/qmc.ts Normal file
View File

@ -0,0 +1,185 @@
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher, QmcStreamCipher } from './qmc_cipher';
import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
import { QmcDeriveKey } from '@/decrypt/qmc_key';
import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
import { extractQQMusicMeta } from '@/utils/qm_meta';
interface Handler {
ext: string;
version: number;
}
export const HandlerMap: { [key: string]: Handler } = {
mgg: { ext: 'ogg', version: 2 },
mgg0: { ext: 'ogg', version: 2 },
mggl: { ext: 'ogg', version: 2 },
mgg1: { ext: 'ogg', version: 2 },
mflac: { ext: 'flac', version: 2 },
mflac0: { ext: 'flac', version: 2 },
mmp4: { ext: 'mmp4', version: 2 },
// qmcflac / qmcogg:
// 有可能是 v2 加密但混用同一个后缀名。
qmcflac: { ext: 'flac', version: 2 },
qmcogg: { ext: 'ogg', version: 2 },
qmc0: { ext: 'mp3', version: 2 },
qmc2: { ext: 'ogg', version: 2 },
qmc3: { ext: 'mp3', version: 2 },
qmc4: { ext: 'ogg', version: 2 },
qmc6: { ext: 'ogg', version: 2 },
qmc8: { ext: 'ogg', version: 2 },
bkcmp3: { ext: 'mp3', version: 1 },
bkcm4a: { ext: 'm4a', version: 1 },
bkcflac: { ext: 'flac', version: 1 },
bkcwav: { ext: 'wav', version: 1 },
bkcape: { ext: 'ape', version: 1 },
bkcogg: { ext: 'ogg', version: 1 },
bkcwma: { ext: 'wma', 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<DecryptResult> {
if (!(raw_ext in HandlerMap)) throw `Qmc cannot handle type: ${raw_ext}`;
const handler = HandlerMap[raw_ext];
let { version } = handler;
const fileBuffer = await GetArrayBuffer(file);
let musicDecoded: Uint8Array | undefined;
let musicID: number | string | undefined;
if (version === 2 && globalThis.WebAssembly) {
console.log('qmc: using wasm decoder');
const v2Decrypted = await DecryptQmcWasm(fileBuffer, raw_ext);
// 若 v2 检测失败,降级到 v1 再尝试一次
if (v2Decrypted.success) {
musicDecoded = v2Decrypted.data;
musicID = v2Decrypted.songId;
console.log('qmc wasm decoder suceeded');
} else {
console.warn('QmcWasm failed with error %s', v2Decrypted.error || '(unknown error)');
}
}
if (!musicDecoded) {
// may throw error
console.log('qmc: using js decoder');
const d = new QmcDecoder(new Uint8Array(fileBuffer));
musicDecoded = d.decrypt();
musicID = d.songID;
}
const ext = SniffAudioExt(musicDecoded, handler.ext);
const mime = AudioMimeType[ext];
const { album, artist, imgUrl, blob, title } = await extractQQMusicMeta(
new Blob([musicDecoded], { type: mime }),
raw_filename,
ext,
musicID,
);
return {
title: title,
artist: artist,
ext: ext,
album: album,
picture: imgUrl,
file: URL.createObjectURL(blob),
blob: blob,
mime: mime,
};
}
export class QmcDecoder {
private static readonly BYTE_COMMA = ','.charCodeAt(0);
private readonly file: Uint8Array;
private readonly size: number;
private decoded: boolean = false;
private audioSize?: number;
private cipher?: QmcStreamCipher;
public constructor(file: Uint8Array) {
this.file = file;
this.size = file.length;
this.searchKey();
}
private _songID?: number;
public get songID() {
return this._songID;
}
public decrypt(): Uint8Array {
if (!this.cipher) {
throw new Error('no cipher found');
}
if (!this.audioSize || this.audioSize <= 0) {
throw new Error('invalid audio size');
}
const audioBuf = this.file.subarray(0, this.audioSize);
if (!this.decoded) {
this.cipher.decrypt(audioBuf, 0);
this.decoded = true;
}
return audioBuf;
}
private searchKey() {
const last4Byte = this.file.slice(-4);
const textEnc = new TextDecoder();
if (textEnc.decode(last4Byte) === 'STag') {
throw new Error('文件中没有写入密钥无法解锁请降级App并重试');
} else 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);
if (keyEnd < 0) {
throw new Error('invalid key: search raw key failed');
}
this.setCipher(rawKey.subarray(0, keyEnd));
const idBuf = rawKey.subarray(keyEnd + 1);
const idEnd = idBuf.findIndex((v) => v == QmcDecoder.BYTE_COMMA);
if (keyEnd < 0) {
throw new Error('invalid key: search song id failed');
}
this._songID = parseInt(textEnc.decode(idBuf.subarray(0, idEnd)), 10);
} else {
const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset);
const keySize = sizeView.getUint32(0, true);
if (keySize < 0x400) {
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();
}
}
}
private setCipher(keyRaw: Uint8Array) {
const keyDec = QmcDeriveKey(keyRaw);
if (keyDec.length > 300) {
this.cipher = new QmcRC4Cipher(keyDec);
} else {
this.cipher = new QmcMapCipher(keyDec);
}
}
}

View File

@ -1,274 +0,0 @@
import {BytesEquals, BytesHasPrefix, FLAC_HEADER, OGG_HEADER} from "@/decrypt/utils.ts";
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,
0x47, 0x95, 0xbb, 0xa1, 0xaa, 0xc6, 0x66, 0x23,
0x92, 0x62, 0xf3, 0x74, 0xa1, 0x9f, 0xf4, 0xa0,
0x1d, 0x3f, 0x5b, 0xf0, 0x13, 0x0e, 0x09, 0x3d,
0xf9, 0xbc, 0x00, 0x11];
class QmcMask {
constructor(matrix, superA, superB) {
if (superA === undefined || superB === undefined) {
if (matrix.length === 44) {
this.Matrix44 = matrix
this.generateMask128from44()
} else {
this.Matrix128 = matrix
this.generateMask44from128()
}
this.generateMask58from128()
} else {
this.Matrix58 = matrix;
this.Super58A = superA;
this.Super58B = superB;
this.generateMask128from58();
this.generateMask44from128()
}
}
generateMask128from58() {
if (this.Matrix58.length !== 56) throw "incorrect mask58 matrix length";
let matrix128 = [];
for (let rowIdx = 0; rowIdx < 8; rowIdx += 1) {
matrix128 = matrix128.concat(
[this.Super58A],
this.Matrix58.slice(7 * rowIdx, 7 * rowIdx + 7),
[this.Super58B],
this.Matrix58.slice(56 - 7 - 7 * rowIdx, 56 - 7 * rowIdx).reverse()
);
}
this.Matrix128 = matrix128;
}
generateMask58from128() {
if (this.Matrix128.length !== 128) throw "incorrect mask128 length";
const superA = this.Matrix128[0], superB = this.Matrix128[8];
let matrix58 = [];
for (let rowIdx = 0; rowIdx < 8; rowIdx += 1) {
let lenStart = 16 * rowIdx;
let lenRightStart = 120 - lenStart;
if (this.Matrix128[lenStart] !== superA || this.Matrix128[lenStart + 8] !== superB) {
throw "decode mask-128 to mask-58 failed"
}
let rowLeft = this.Matrix128.slice(lenStart + 1, lenStart + 8);
let rowRight = this.Matrix128.slice(lenRightStart + 1, lenRightStart + 8).reverse();
if (BytesEquals(rowLeft, rowRight)) {
matrix58 = matrix58.concat(rowLeft);
} else {
throw "decode mask-128 to mask-58 failed"
}
}
this.Matrix58 = matrix58;
this.Super58A = superA;
this.Super58B = superB;
}
generateMask44from128() {
if (this.Matrix128.length !== 128) throw "incorrect mask128 matrix length";
let mapping = GetConvertMapping()
this.Matrix44 = []
let idxI44 = 0
mapping.forEach(it256 => {
let it256Len = it256.length
for (let i = 1; i < it256Len; i++) {
if (this.Matrix128[it256[0]] !== this.Matrix128[it256[i]]) {
throw "decode mask-128 to mask-44 failed"
}
}
this.Matrix44[idxI44] = this.Matrix128[it256[0]]
idxI44++
})
}
generateMask128from44() {
if (this.Matrix44.length !== 44) throw "incorrect mask length"
this.Matrix128 = []
let idx44 = 0
GetConvertMapping().forEach(it256 => {
it256.forEach(m => {
this.Matrix128[m] = this.Matrix44[idx44]
})
idx44++
})
}
Decrypt(data) {
let dst = data.slice(0);
let index = -1;
let maskIdx = -1;
for (let cur = 0; cur < data.length; cur++) {
index++;
maskIdx++;
if (index === 0x8000 || (index > 0x8000 && (index + 1) % 0x8000 === 0)) {
index++;
maskIdx++;
}
if (maskIdx >= 128) maskIdx -= 128;
dst[cur] ^= this.Matrix128[maskIdx];
}
return dst;
}
}
export function QmcMaskGetDefault() {
return new QmcMask(QMCDefaultMaskMatrix)
}
export function QmcMaskDetectMflac(data) {
let search_len = Math.min(0x8000, data.length), mask;
for (let block_idx = 0; block_idx < search_len; block_idx += 128) {
try {
mask = new QmcMask(data.slice(block_idx, block_idx + 128));
if (BytesHasPrefix(mask.Decrypt(data.slice(0, FLAC_HEADER.length)), FLAC_HEADER)) {
break;
}
} catch (e) {
}
}
return mask;
}
export function QmcMaskDetectMgg(data) {
if (data.length < 0x100) return
let matrixConfidence = {};
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 = GetMask44Index(idx128);
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] = getMaskConfidenceResult(matrixConfidence[i]);
} catch (e) {
return;
}
const mask = new QmcMask(matrix);
if (!BytesHasPrefix(mask.Decrypt(data.slice(0, OGG_HEADER.length)), OGG_HEADER)) {
return;
}
return mask;
}
export function QmcMaskCreate128(mask128) {
return new QmcMask(mask128)
}
export function QmcMaskCreate58(matrix, superA, superB) {
return new QmcMask(matrix, superA, superB)
}
export function QmcMaskCreate44(mask44) {
return new QmcMask(mask44)
}
/**
* @param confidence {{}}
* @returns {number}
*/
function getMaskConfidenceResult(confidence) {
if (confidence.length === 0) throw "can not match at least one key";
if (confidence.length > 1) console.warn("There are 2 potential value for the mask!")
let result, conf = 0;
for (let idx in confidence) {
if (confidence[idx] > conf) {
result = idx;
conf = confidence[idx];
}
}
return parseInt(result)
}
/**
* @return {number}
*/
const allMapping = [];
const mask128to44 = [];
(function () {
for (let i = 0; i < 128; i++) {
let realIdx = (i * i + 27) % 256
if (realIdx in allMapping) {
allMapping[realIdx].push(i)
} else {
allMapping[realIdx] = [i]
}
}
let idx44 = 0
allMapping.forEach(all128 => {
all128.forEach(_i128 => {
mask128to44[_i128] = idx44
})
idx44++
})
})();
function GetConvertMapping() {
return allMapping;
}
function GetMask44Index(idx128) {
return mask128to44[idx128 % 128]
}
function QmcGenerateOggHeader(page2) {
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) {
let specConf = [6, 0]
for (let i = 2; i < page2; i++) specConf.push(4)
specConf.push(0)
return QMOggPublicConf1.concat(specConf, QMOggPublicConf2)
}

View File

@ -0,0 +1,117 @@
import { QmcMapCipher, QmcRC4Cipher, QmcStaticCipher } from '@/decrypt/qmc_cipher';
import fs from 'fs';
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);
expect(buf).toStrictEqual(expected);
});
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);
expect(buf).toStrictEqual(expected);
});
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 c = new QmcMapCipher(key);
c.decrypt(buf, 0);
expect(buf).toStrictEqual(expected);
});
function loadTestDataCipher(name: string): {
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`),
};
}
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);
c.decrypt(cipherText, 0);
expect(cipherText).toStrictEqual(clearText);
}
});
test('rc4 cipher: real file', async () => {
const cases = ['mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
const { key, clearText, cipherText } = loadTestDataCipher(name);
const c = new QmcRC4Cipher(key);
c.decrypt(cipherText, 0);
expect(cipherText).toStrictEqual(clearText);
}
});
test('rc4 cipher: first segment', async () => {
const cases = ['mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
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));
}
});
test('rc4 cipher: align block (128~5120)', async () => {
const cases = ['mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
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));
}
});
test('rc4 cipher: simple block (5120~10240)', async () => {
const cases = ['mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
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));
}
});

199
src/decrypt/qmc_cipher.ts Normal file
View File

@ -0,0 +1,199 @@
export interface QmcStreamCipher {
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
0x9E, 0xE6, 0x9D, 0xCF, 0xFA, 0x7F, 0x14, 0xD1, //0x10
0xCE, 0xB8, 0xDC, 0xC3, 0x4A, 0x67, 0x93, 0xD6, //0x18
0x28, 0xC2, 0x91, 0x70, 0xCA, 0x8D, 0xA2, 0xA4, //0x20
0xF0, 0x08, 0x61, 0x90, 0x7E, 0x6F, 0xA2, 0xE0, //0x28
0xEB, 0xAE, 0x3E, 0xB6, 0x67, 0xC7, 0x92, 0xF4, //0x30
0x91, 0xB5, 0xF6, 0x6C, 0x5E, 0x84, 0x40, 0xF7, //0x38
0xF3, 0x1B, 0x02, 0x7F, 0xD5, 0xAB, 0x41, 0x89, //0x40
0x28, 0xF4, 0x25, 0xCC, 0x52, 0x11, 0xAD, 0x43, //0x48
0x68, 0xA6, 0x41, 0x8B, 0x84, 0xB5, 0xFF, 0x2C, //0x50
0x92, 0x4A, 0x26, 0xD8, 0x47, 0x6A, 0x7C, 0x95, //0x58
0x61, 0xCC, 0xE6, 0xCB, 0xBB, 0x3F, 0x47, 0x58, //0x60
0x89, 0x75, 0xC3, 0x75, 0xA1, 0xD9, 0xAF, 0xCC, //0x68
0x08, 0x73, 0x17, 0xDC, 0xAA, 0x9A, 0xA2, 0x16, //0x70
0x41, 0xD8, 0xA2, 0x06, 0xC6, 0x8B, 0xFC, 0x66, //0x78
0x34, 0x9F, 0xCF, 0x18, 0x23, 0xA0, 0x0A, 0x74, //0x80
0xE7, 0x2B, 0x27, 0x70, 0x92, 0xE9, 0xAF, 0x37, //0x88
0xE6, 0x8C, 0xA7, 0xBC, 0x62, 0x65, 0x9C, 0xC2, //0x90
0x08, 0xC9, 0x88, 0xB3, 0xF3, 0x43, 0xAC, 0x74, //0x98
0x2C, 0x0F, 0xD4, 0xAF, 0xA1, 0xC3, 0x01, 0x64, //0xA0
0x95, 0x4E, 0x48, 0x9F, 0xF4, 0x35, 0x78, 0x95, //0xA8
0x7A, 0x39, 0xD6, 0x6A, 0xA0, 0x6D, 0x40, 0xE8, //0xB0
0x4F, 0xA8, 0xEF, 0x11, 0x1D, 0xF3, 0x1B, 0x3F, //0xB8
0x3F, 0x07, 0xDD, 0x6F, 0x5B, 0x19, 0x30, 0x19, //0xC0
0xFB, 0xEF, 0x0E, 0x37, 0xF0, 0x0E, 0xCD, 0x16, //0xC8
0x49, 0xFE, 0x53, 0x47, 0x13, 0x1A, 0xBD, 0xA4, //0xD0
0xF1, 0x40, 0x19, 0x60, 0x0E, 0xED, 0x68, 0x09, //0xD8
0x06, 0x5F, 0x4D, 0xCF, 0x3D, 0x1A, 0xFE, 0x20, //0xE0
0x77, 0xE4, 0xD9, 0xDA, 0xF9, 0xA4, 0x2B, 0x76, //0xE8
0x1C, 0x71, 0xDB, 0x00, 0xBC, 0xFD, 0x0C, 0x6C, //0xF0
0xA5, 0x47, 0xF7, 0xF6, 0x00, 0x79, 0x4A, 0x11, //0xF8
])
public getMask(offset: number) {
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);
}
}
}
export class QmcMapCipher implements QmcStreamCipher {
key: Uint8Array;
n: number;
constructor(key: Uint8Array) {
if (key.length == 0) throw Error('qmc/cipher_map: invalid key size');
this.key = key;
this.n = key.length;
}
private static rotate(value: number, bits: number) {
let rotate = (bits + 4) % 8;
let left = value << rotate;
let right = value >> rotate;
return (left | right) & 0xff;
}
decrypt(buf: Uint8Array, offset: number): void {
for (let i = 0; i < buf.length; i++) {
buf[i] ^= this.getMask(offset + i);
}
}
private getMask(offset: number) {
if (offset > 0x7fff) offset %= 0x7fff;
const idx = (offset * offset + 71214) % this.n;
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;
S: Uint8Array;
N: number;
key: Uint8Array;
hash: number;
constructor(key: Uint8Array) {
if (key.length == 0) {
throw Error('invalid key size');
}
this.key = key;
this.N = key.length;
// init seed box
this.S = new Uint8Array(this.N);
for (let i = 0; i < this.N; ++i) {
this.S[i] = i & 0xff;
}
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]];
}
// init hash base
this.hash = 1;
for (let i = 0; i < this.N; i++) {
let value = this.key[i];
// ignore if key char is '\x00'
if (!value) continue;
const next_hash = (this.hash * value) >>> 0;
if (next_hash == 0 || next_hash <= this.hash) break;
this.hash = next_hash;
}
}
decrypt(buf: Uint8Array, offset: number): void {
let toProcess = buf.length;
let processed = 0;
const postProcess = (len: number): boolean => {
toProcess -= len;
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;
}
// 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;
}
// Batch process segments
while (toProcess > QmcRC4Cipher.SEGMENT_SIZE) {
this.encASegment(buf.subarray(processed, processed + QmcRC4Cipher.SEGMENT_SIZE), offset);
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);
// 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(Math.floor(offset / QmcRC4Cipher.SEGMENT_SIZE));
// decrypt the block
let j = 0;
let k = 0;
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]];
if (i >= 0) {
buf[i] ^= S[(S[j] + S[k]) % this.N];
}
}
}
private getSegmentKey(id: number): number {
const seed = this.key[id % this.N];
const idx = Math.floor((this.hash / ((id + 1) * seed)) * 100.0);
return idx % this.N;
}
}

View File

@ -0,0 +1,26 @@
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]);
});
function loadTestDataKeyDecrypt(name: string): {
cipherText: Uint8Array;
clearText: Uint8Array;
} {
return {
cipherText: fs.readFileSync(`testdata/${name}_key_raw.bin`),
clearText: fs.readFileSync(`testdata/${name}_key.bin`),
};
}
test('key dec: real file', async () => {
const cases = ['mflac_map', 'mgg_map', 'mflac0_rc4', 'mflac_rc4'];
for (const name of cases) {
const { clearText, cipherText } = loadTestDataKeyDecrypt(name);
const buf = QmcDeriveKey(cipherText);
expect(buf).toStrictEqual(clearText);
}
});

127
src/decrypt/qmc_key.ts Normal file
View File

@ -0,0 +1,127 @@
import { TeaCipher } from '@/utils/tea';
const SALT_LEN = 2;
const ZERO_LEN = 7;
export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
const textDec = new TextDecoder();
let rawDec = Buffer.from(textDec.decode(raw), 'base64');
let n = rawDec.length;
if (n < 16) {
throw Error('key length is too short');
}
rawDec = decryptV2Key(rawDec);
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);
}
// simpleMakeKey exported only for unit test
export function simpleMakeKey(salt: number, length: number): 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);
}
return keyBuf;
}
const mixKey1: Uint8Array = new Uint8Array([ 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 ])
const mixKey2: Uint8Array = new Uint8Array([ 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 ])
function decryptV2Key(key: Buffer): Buffer
{
const textEnc = new TextDecoder();
if (key.length < 18 || textEnc.decode(key.slice(0, 18)) !== 'QQMusic EncV2,Key:') {
return key;
}
let out = decryptTencentTea(key.slice(18), mixKey1);
out = decryptTencentTea(out, mixKey2);
const textDec = new TextDecoder();
const keyDec = Buffer.from(textDec.decode(out), 'base64');
let n = keyDec.length;
if (n < 16) {
throw Error('EncV2 key decode failed');
}
return keyDec;
}
function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
if (inBuf.length % 8 != 0) {
throw Error('inBuf size not a multiple of the block size');
}
if (inBuf.length < 16) {
throw Error('inBuf size too small');
}
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));
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);
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);
for (let j = 0; j < 8; j++) {
tmpBuf[j] ^= ivCur[j];
}
blk.decrypt(tmpView, tmpView);
inBufPos += 8;
tmpIdx = 0;
};
// 跳过 Salt
for (let i = 1; i <= SALT_LEN; ) {
if (tmpIdx < 8) {
tmpIdx++;
i++;
} else {
cryptBlock();
}
}
// 还原明文
let outBufPos = 0;
while (outBufPos < outLen) {
if (tmpIdx < 8) {
outBuf[outBufPos] = tmpBuf[tmpIdx] ^ ivPrev[tmpIdx];
outBufPos++;
tmpIdx++;
} else {
cryptBlock();
}
}
// 校验Zero
for (let i = 1; i <= ZERO_LEN; i++) {
if (tmpBuf[tmpIdx] != ivPrev[tmpIdx]) {
throw Error('zero check failed');
}
}
return outBuf;
}

73
src/decrypt/qmc_wasm.ts Normal file
View File

@ -0,0 +1,73 @@
import QmcCryptoModule from '@/QmcWasm/QmcWasmBundle';
import { MergeUint8Array } from '@/utils/MergeUint8Array';
// 每次处理 2M 的数据
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
export interface QMCDecryptionResult {
success: boolean;
data: Uint8Array;
songId: string | number;
error: string;
}
/**
* QMC
*
* Uint8Array
* @param {ArrayBuffer} qmcBlob Blob
*/
export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise<QMCDecryptionResult> {
const result: QMCDecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
// 初始化模组
let QmcCrypto: any;
try {
QmcCrypto = await QmcCryptoModule();
} catch (err: any) {
result.error = err?.message || 'wasm 加载失败';
return result;
}
if (!QmcCrypto) {
result.error = 'wasm 加载失败';
return result;
}
// 申请内存块,并文件末端数据到 WASM 的内存堆
const qmcBuf = new Uint8Array(qmcBlob);
const pQmcBuf = QmcCrypto._malloc(DECRYPTION_BUF_SIZE);
QmcCrypto.writeArrayToMemory(qmcBuf.slice(-DECRYPTION_BUF_SIZE), pQmcBuf);
// 进行解密初始化
ext = '.' + ext;
const tailSize = QmcCrypto.preDec(pQmcBuf, DECRYPTION_BUF_SIZE, ext);
if (tailSize == -1) {
result.error = QmcCrypto.getError();
return result;
} else {
result.songId = QmcCrypto.getSongId();
result.songId = result.songId == "0" ? 0 : result.songId;
}
const decryptedParts = [];
let offset = 0;
let bytesToDecrypt = qmcBuf.length - tailSize;
while (bytesToDecrypt > 0) {
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
// 解密一些片段
const blockData = new Uint8Array(qmcBuf.slice(offset, offset + blockSize));
QmcCrypto.writeArrayToMemory(blockData, pQmcBuf);
decryptedParts.push(QmcCrypto.HEAPU8.slice(pQmcBuf, pQmcBuf + QmcCrypto.decBlob(pQmcBuf, blockSize, offset)));
offset += blockSize;
bytesToDecrypt -= blockSize;
}
QmcCrypto._free(pQmcBuf);
result.data = MergeUint8Array(decryptedParts);
result.success = true;
return result;
}

67
src/decrypt/qmccache.ts Normal file
View File

@ -0,0 +1,67 @@
import {
AudioMimeType,
GetArrayBuffer,
GetCoverFromFile,
GetMetaFromFile,
SniffAudioExt,
SplitFilename,
} from '@/decrypt/utils';
import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc';
import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
import { DecryptResult } from '@/decrypt/entity';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const buffer = await GetArrayBuffer(file);
let musicDecoded: Uint8Array | undefined;
if (globalThis.WebAssembly) {
console.log('qmc: using wasm decoder');
const qmcDecrypted = await DecryptQmcWasm(buffer, raw_ext);
// 若 wasm 失败,使用 js 再尝试一次
if (qmcDecrypted.success) {
musicDecoded = qmcDecrypted.data;
console.log('qmc wasm decoder suceeded');
} else {
console.warn('QmcWasm failed with error %s', qmcDecrypted.error || '(unknown error)');
}
}
if (!musicDecoded) {
musicDecoded = new Uint8Array(buffer);
let length = musicDecoded.length;
for (let i = 0; i < length; i++) {
let byte = musicDecoded[i] ^ 0xf4; // xor 0xf4
byte = ((byte & 0b0011_1111) << 2) | (byte >> 6); // rol 2
musicDecoded[i] = byte;
}
}
let ext = SniffAudioExt(musicDecoded, '');
const newName = SplitFilename(raw_filename);
let audioBlob: Blob;
if (ext !== '' || newName.ext === 'mp3') {
audioBlob = new Blob([musicDecoded], { type: AudioMimeType[ext] });
} else if (newName.ext in HandlerMap) {
audioBlob = new Blob([musicDecoded], { 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, String(tag.common.artists || tag.common.artist || ""));
return {
title,
artist,
ext,
album: tag.common.album,
picture: GetCoverFromFile(tag),
file: URL.createObjectURL(audioBlob),
blob: audioBlob,
mime: AudioMimeType[ext],
};
}

View File

@ -1,28 +1,32 @@
import {AudioMimeType, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile, SniffAudioExt} from "@/decrypt/utils.ts";
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<DecryptResult> {
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<DecryptResult> {
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, String(tag.common.artists || tag.common.artist || ''));
return {
status: true,
title,
artist,
ext,
album: tag.common.album,
picture: GetCoverFromFile(tag),
file: URL.createObjectURL(file),
mime: AudioMimeType[ext]
}
return {
title,
artist,
ext,
album: tag.common.album,
picture: GetCoverFromFile(tag),
file: URL.createObjectURL(file),
blob: file,
mime: AudioMimeType[ext],
};
}

View File

@ -1,14 +1,14 @@
import {Decrypt as RawDecrypt} from "./raw";
import {GetArrayBuffer} from "@/decrypt/utils.ts";
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<DecryptResult> {
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);
}

View File

@ -1,51 +0,0 @@
const ID3Writer = require("browser-id3-writer");
export const IXAREA_API_ENDPOINT = "https://stats.ixarea.com/apis"
export async function GetWebImage(pic_url) {
try {
let resp = await fetch(pic_url);
let mime = resp.headers.get("Content-Type");
if (mime.startsWith("image/")) {
let buf = await resp.arrayBuffer();
let objBlob = new Blob([buf], {type: mime});
let objUrl = URL.createObjectURL(objBlob);
return {"buffer": buf, "src": pic_url, "url": objUrl, "type": mime};
}
} catch (e) {
}
return {"buffer": null, "src": pic_url, "url": "", "type": ""}
}
export async function WriteMp3Meta(audioData, artistList, title, album, pictureData = null, pictureDesc = "Cover", originalMeta = null) {
const writer = new ID3Writer(audioData);
if (originalMeta !== null) {
artistList = originalMeta.common.artists || artistList
title = originalMeta.common.title || title
album = originalMeta.common.album || album
const frames = originalMeta.native['ID3v2.4'] || originalMeta.native['ID3v2.3'] || originalMeta.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) {
}
}
})
}
writer.setFrame('TPE1', artistList)
.setFrame('TIT2', title)
.setFrame('TALB', album);
if (pictureData !== null) {
writer.setFrame('APIC', {
type: 3,
data: pictureData,
description: pictureDesc,
})
}
writer.addTag();
return writer.arrayBuffer;
}

View File

@ -1,91 +1,262 @@
import {IAudioMetadata} from "music-metadata-browser";
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 split_regex = /[ ]?[,;/_、][ ]?/;
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];
//prettier-ignore
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]
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"
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 BytesEquals(data: Uint8Array, another: Uint8Array): boolean {
if (another.length != data.length) return false
return data.every((val, idx) => {
return val === another[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"
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<ArrayBuffer> {
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) {
//由文件名和原metadata共同决定歌手tag(有时从文件名看有多个歌手而metadata只有一个)
if (!meta.artist || meta.artist.split(split_regex).length < items[0].trim().split(split_regex).length) 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 interface IMusicMeta {
title: string;
artists?: string[];
album?: string;
albumartist?: string;
genre?: string[];
picture?: ArrayBuffer;
picture_desc?: string;
}
export function WriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
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) {
console.warn(`failed to write ID3 tag '${frame.id}'`);
}
}
});
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 || '',
});
}
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));
}
}
if (info.picture) {
writer.importPictureFromBuffer(Buffer.from(info.picture));
}
return writer.save();
}
export function RewriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
const writer = new ID3Writer(audioData);
// preserve 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'
&& frame.id !== 'TPE2'
&& frame.id !== 'TCON'
) {
try {
writer.setFrame(frame.id, frame.value);
} catch (e) {
throw new Error(`failed to write ID3 tag '${frame.id}'`);
}
}
});
const old = original.common;
writer
.setFrame('TPE1', info?.artists || old.artists || [])
.setFrame('TIT2', info?.title || old.title)
.setFrame('TALB', info?.album || old.album || '')
.setFrame('TPE2', info?.albumartist || old.albumartist || '')
.setFrame('TCON', info?.genre || old.genre || []);
if (info.picture) {
writer.setFrame('APIC', {
type: 3,
data: info.picture,
description: info.picture_desc || '',
});
}
return writer.addTag();
}
export function RewriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
const writer = new MetaFlac(audioData);
const old = original.common;
if (info.title) {
if (old.title) {
writer.removeTag('TITLE');
}
writer.setTag('TITLE=' + info.title);
}
if (info.album) {
if (old.album) {
writer.removeTag('ALBUM');
}
writer.setTag('ALBUM=' + info.album);
}
if (info.albumartist) {
if (old.albumartist) {
writer.removeTag('ALBUMARTIST');
}
writer.setTag('ALBUMARTIST=' + info.albumartist);
}
if (info.artists) {
if (old.artists) {
writer.removeTag('ARTIST');
}
info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist));
}
if (info.genre) {
if (old.genre) {
writer.removeTag('GENRE');
}
info.genre.forEach((singlegenre) => writer.setTag('GENRE=' + singlegenre));
}
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),
};
}

193
src/decrypt/ximalaya.ts Normal file
View File

@ -0,0 +1,193 @@
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
import { AudioMimeType, SniffAudioExt, GetArrayBuffer, GetMetaFromFile } from './utils';
import { DecryptResult } from '@/decrypt/entity';
const HandlerMap: Map<string, (data: Uint8Array) => Uint8Array> = new Map([
['x2m', ProcessX2M],
['x3m', ProcessX3M],
]);
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const buffer = new Uint8Array(await GetArrayBuffer(file));
const handler = HandlerMap.get(raw_ext);
if (!handler) throw 'File type is incorrect!';
let musicDecoded: Uint8Array = handler(buffer);
const ext = SniffAudioExt(musicDecoded, 'm4a');
const mime = AudioMimeType[ext];
let musicBlob = new Blob([musicDecoded], { type: mime });
const musicMeta = await metaParseBlob(musicBlob);
const info = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
return {
picture: '',
title: info.title,
artist: info.artist,
ext: ext,
album: musicMeta.common.album,
blob: musicBlob,
file: URL.createObjectURL(musicBlob),
mime: mime,
};
}
function ProcessX2M(data: Uint8Array) {
const x2mHeaderSize = 1024;
const x2mKey = [0x78, 0x6d, 0x6c, 0x79];
let encryptedHeader = data.slice(0, x2mHeaderSize);
for (let idx = 0; idx < x2mHeaderSize; idx++) {
let srcIdx = x2mScrambleTable[idx];
data[idx] = encryptedHeader[srcIdx] ^ x2mKey[idx % x2mKey.length];
}
return data;
}
function ProcessX3M(data: Uint8Array) {
const x3mHeaderSize = 1024;
//prettier-ignore: uint8, size 32(8x4)
const x3mKey = [
0x33, 0x39, 0x38, 0x39, 0x64, 0x31, 0x31, 0x31, 0x61, 0x61, 0x64, 0x35, 0x36, 0x31, 0x33, 0x39, 0x34, 0x30, 0x66,
0x34, 0x66, 0x63, 0x34, 0x34, 0x62, 0x36, 0x33, 0x39, 0x62, 0x32, 0x39, 0x32,
];
let encryptedHeader = data.slice(0, x3mHeaderSize);
for (let dstIdx = 0; dstIdx < x3mHeaderSize; dstIdx++) {
let srcIdx = x3mScrambleTable[dstIdx];
data[dstIdx] = encryptedHeader[srcIdx] ^ x3mKey[dstIdx % x3mKey.length];
}
return data;
}
//prettier-ignore: uint16, size 1024 (64x16)
const x2mScrambleTable = [
0x2a9, 0x2ab, 0x154, 0x2aa, 0x2a8, 0x2ac, 0x153, 0x2a7, 0x2ad, 0x152, 0x2a6, 0x3ff, 0x000, 0x155, 0x2ae, 0x151, 0x2a5,
0x3fe, 0x001, 0x156, 0x2af, 0x150, 0x2a4, 0x3fd, 0x002, 0x157, 0x2b0, 0x14f, 0x2a3, 0x3fc, 0x003, 0x158, 0x2b1, 0x14e,
0x2a2, 0x3fb, 0x004, 0x159, 0x2b2, 0x14d, 0x2a1, 0x3fa, 0x005, 0x15a, 0x2b3, 0x14c, 0x2a0, 0x3f9, 0x006, 0x15b, 0x2b4,
0x14b, 0x29f, 0x3f8, 0x007, 0x15c, 0x2b5, 0x14a, 0x29e, 0x3f7, 0x008, 0x15d, 0x2b6, 0x149, 0x29d, 0x3f6, 0x009, 0x15e,
0x2b7, 0x148, 0x29c, 0x3f5, 0x00a, 0x15f, 0x2b8, 0x147, 0x29b, 0x3f4, 0x00b, 0x160, 0x2b9, 0x146, 0x29a, 0x3f3, 0x00c,
0x161, 0x2ba, 0x145, 0x299, 0x3f2, 0x00d, 0x162, 0x2bb, 0x144, 0x298, 0x3f1, 0x00e, 0x163, 0x2bc, 0x143, 0x297, 0x3f0,
0x00f, 0x164, 0x2bd, 0x142, 0x296, 0x3ef, 0x010, 0x165, 0x2be, 0x141, 0x295, 0x3ee, 0x011, 0x166, 0x2bf, 0x140, 0x294,
0x3ed, 0x012, 0x167, 0x2c0, 0x13f, 0x293, 0x3ec, 0x013, 0x168, 0x2c1, 0x13e, 0x292, 0x3eb, 0x014, 0x169, 0x2c2, 0x13d,
0x291, 0x3ea, 0x015, 0x16a, 0x2c3, 0x13c, 0x290, 0x3e9, 0x016, 0x16b, 0x2c4, 0x13b, 0x28f, 0x3e8, 0x017, 0x16c, 0x2c5,
0x13a, 0x28e, 0x3e7, 0x018, 0x16d, 0x2c6, 0x139, 0x28d, 0x3e6, 0x019, 0x16e, 0x2c7, 0x138, 0x28c, 0x3e5, 0x01a, 0x16f,
0x2c8, 0x137, 0x28b, 0x3e4, 0x01b, 0x170, 0x2c9, 0x136, 0x28a, 0x3e3, 0x01c, 0x171, 0x2ca, 0x135, 0x289, 0x3e2, 0x01d,
0x172, 0x2cb, 0x134, 0x288, 0x3e1, 0x01e, 0x173, 0x2cc, 0x133, 0x287, 0x3e0, 0x01f, 0x174, 0x2cd, 0x0a9, 0x1fe, 0x357,
0x020, 0x175, 0x2ce, 0x0aa, 0x1ff, 0x358, 0x021, 0x176, 0x2cf, 0x0ab, 0x200, 0x359, 0x022, 0x177, 0x2d0, 0x0ac, 0x201,
0x35a, 0x023, 0x178, 0x2d1, 0x0ad, 0x202, 0x35b, 0x024, 0x179, 0x2d2, 0x0ae, 0x203, 0x35c, 0x025, 0x17a, 0x2d3, 0x0af,
0x204, 0x35d, 0x026, 0x17b, 0x2d4, 0x0b0, 0x205, 0x35e, 0x027, 0x17c, 0x2d5, 0x0b1, 0x206, 0x35f, 0x028, 0x17d, 0x2d6,
0x0b2, 0x207, 0x360, 0x029, 0x17e, 0x2d7, 0x0b3, 0x208, 0x361, 0x02a, 0x17f, 0x2d8, 0x0b4, 0x209, 0x362, 0x02b, 0x180,
0x2d9, 0x0b5, 0x20a, 0x363, 0x02c, 0x181, 0x2da, 0x0b6, 0x20b, 0x364, 0x02d, 0x182, 0x2db, 0x0b7, 0x20c, 0x365, 0x02e,
0x183, 0x2dc, 0x0b8, 0x20d, 0x366, 0x02f, 0x184, 0x2dd, 0x0b9, 0x20e, 0x367, 0x030, 0x185, 0x2de, 0x0ba, 0x20f, 0x368,
0x031, 0x186, 0x2df, 0x0bb, 0x210, 0x369, 0x032, 0x187, 0x2e0, 0x0bc, 0x211, 0x36a, 0x033, 0x188, 0x2e1, 0x0bd, 0x212,
0x36b, 0x034, 0x189, 0x2e2, 0x0be, 0x213, 0x36c, 0x035, 0x18a, 0x2e3, 0x0bf, 0x214, 0x36d, 0x036, 0x18b, 0x2e4, 0x0c0,
0x215, 0x36e, 0x037, 0x18c, 0x2e5, 0x0c1, 0x216, 0x36f, 0x038, 0x18d, 0x2e6, 0x0c2, 0x217, 0x370, 0x039, 0x18e, 0x2e7,
0x0c3, 0x218, 0x371, 0x03a, 0x18f, 0x2e8, 0x0c4, 0x219, 0x372, 0x03b, 0x190, 0x2e9, 0x0c5, 0x21a, 0x373, 0x03c, 0x191,
0x2ea, 0x0c6, 0x21b, 0x374, 0x03d, 0x192, 0x2eb, 0x0c7, 0x21c, 0x375, 0x03e, 0x193, 0x2ec, 0x0c8, 0x21d, 0x376, 0x03f,
0x194, 0x2ed, 0x0c9, 0x21e, 0x377, 0x040, 0x195, 0x2ee, 0x0ca, 0x21f, 0x378, 0x041, 0x196, 0x2ef, 0x0cb, 0x220, 0x379,
0x042, 0x197, 0x2f0, 0x0cc, 0x221, 0x37a, 0x043, 0x198, 0x2f1, 0x0cd, 0x222, 0x37b, 0x044, 0x199, 0x2f2, 0x0ce, 0x223,
0x37c, 0x045, 0x19a, 0x2f3, 0x0cf, 0x224, 0x37d, 0x046, 0x19b, 0x2f4, 0x0d0, 0x225, 0x37e, 0x047, 0x19c, 0x2f5, 0x0d1,
0x226, 0x37f, 0x048, 0x19d, 0x2f6, 0x0d2, 0x227, 0x380, 0x049, 0x19e, 0x2f7, 0x0d3, 0x228, 0x381, 0x04a, 0x19f, 0x2f8,
0x0d4, 0x229, 0x382, 0x04b, 0x1a0, 0x2f9, 0x0d5, 0x22a, 0x383, 0x04c, 0x1a1, 0x2fa, 0x0d6, 0x22b, 0x384, 0x04d, 0x1a2,
0x2fb, 0x0d7, 0x22c, 0x385, 0x04e, 0x1a3, 0x2fc, 0x0d8, 0x22d, 0x386, 0x04f, 0x1a4, 0x2fd, 0x0d9, 0x22e, 0x387, 0x050,
0x1a5, 0x2fe, 0x0da, 0x22f, 0x388, 0x051, 0x1a6, 0x2ff, 0x0db, 0x230, 0x389, 0x052, 0x1a7, 0x300, 0x0dc, 0x231, 0x38a,
0x053, 0x1a8, 0x301, 0x0dd, 0x232, 0x38b, 0x054, 0x1a9, 0x302, 0x0de, 0x233, 0x38c, 0x055, 0x1aa, 0x303, 0x0df, 0x234,
0x38d, 0x056, 0x1ab, 0x304, 0x0e0, 0x235, 0x38e, 0x057, 0x1ac, 0x305, 0x0e1, 0x236, 0x38f, 0x058, 0x1ad, 0x306, 0x0e2,
0x237, 0x390, 0x059, 0x1ae, 0x307, 0x0e3, 0x238, 0x391, 0x05a, 0x1af, 0x308, 0x0e4, 0x239, 0x392, 0x05b, 0x1b0, 0x309,
0x0e5, 0x23a, 0x393, 0x05c, 0x1b1, 0x30a, 0x0e6, 0x23b, 0x394, 0x05d, 0x1b2, 0x30b, 0x0e7, 0x23c, 0x395, 0x05e, 0x1b3,
0x30c, 0x0e8, 0x23d, 0x396, 0x05f, 0x1b4, 0x30d, 0x0e9, 0x23e, 0x397, 0x060, 0x1b5, 0x30e, 0x0ea, 0x23f, 0x398, 0x061,
0x1b6, 0x30f, 0x0eb, 0x240, 0x399, 0x062, 0x1b7, 0x310, 0x0ec, 0x241, 0x39a, 0x063, 0x1b8, 0x311, 0x0ed, 0x242, 0x39b,
0x064, 0x1b9, 0x312, 0x0ee, 0x243, 0x39c, 0x065, 0x1ba, 0x313, 0x0ef, 0x244, 0x39d, 0x066, 0x1bb, 0x314, 0x0f0, 0x245,
0x39e, 0x067, 0x1bc, 0x315, 0x0f1, 0x246, 0x39f, 0x068, 0x1bd, 0x316, 0x0f2, 0x247, 0x3a0, 0x069, 0x1be, 0x317, 0x0f3,
0x248, 0x3a1, 0x06a, 0x1bf, 0x318, 0x0f4, 0x249, 0x3a2, 0x06b, 0x1c0, 0x319, 0x0f5, 0x24a, 0x3a3, 0x06c, 0x1c1, 0x31a,
0x0f6, 0x24b, 0x3a4, 0x06d, 0x1c2, 0x31b, 0x0f7, 0x24c, 0x3a5, 0x06e, 0x1c3, 0x31c, 0x0f8, 0x24d, 0x3a6, 0x06f, 0x1c4,
0x31d, 0x0f9, 0x24e, 0x3a7, 0x070, 0x1c5, 0x31e, 0x0fa, 0x24f, 0x3a8, 0x071, 0x1c6, 0x31f, 0x0fb, 0x250, 0x3a9, 0x072,
0x1c7, 0x320, 0x0fc, 0x251, 0x3aa, 0x073, 0x1c8, 0x321, 0x0fd, 0x252, 0x3ab, 0x074, 0x1c9, 0x322, 0x0fe, 0x253, 0x3ac,
0x075, 0x1ca, 0x323, 0x0ff, 0x254, 0x3ad, 0x076, 0x1cb, 0x324, 0x100, 0x255, 0x3ae, 0x077, 0x1cc, 0x325, 0x101, 0x256,
0x3af, 0x078, 0x1cd, 0x326, 0x102, 0x257, 0x3b0, 0x079, 0x1ce, 0x327, 0x103, 0x258, 0x3b1, 0x07a, 0x1cf, 0x328, 0x104,
0x259, 0x3b2, 0x07b, 0x1d0, 0x329, 0x105, 0x25a, 0x3b3, 0x07c, 0x1d1, 0x32a, 0x106, 0x25b, 0x3b4, 0x07d, 0x1d2, 0x32b,
0x107, 0x25c, 0x3b5, 0x07e, 0x1d3, 0x32c, 0x108, 0x25d, 0x3b6, 0x07f, 0x1d4, 0x32d, 0x109, 0x25e, 0x3b7, 0x080, 0x1d5,
0x32e, 0x10a, 0x25f, 0x3b8, 0x081, 0x1d6, 0x32f, 0x10b, 0x260, 0x3b9, 0x082, 0x1d7, 0x330, 0x10c, 0x261, 0x3ba, 0x083,
0x1d8, 0x331, 0x10d, 0x262, 0x3bb, 0x084, 0x1d9, 0x332, 0x10e, 0x263, 0x3bc, 0x085, 0x1da, 0x333, 0x10f, 0x264, 0x3bd,
0x086, 0x1db, 0x334, 0x110, 0x265, 0x3be, 0x087, 0x1dc, 0x335, 0x111, 0x266, 0x3bf, 0x088, 0x1dd, 0x336, 0x112, 0x267,
0x3c0, 0x089, 0x1de, 0x337, 0x113, 0x268, 0x3c1, 0x08a, 0x1df, 0x338, 0x114, 0x269, 0x3c2, 0x08b, 0x1e0, 0x339, 0x115,
0x26a, 0x3c3, 0x08c, 0x1e1, 0x33a, 0x116, 0x26b, 0x3c4, 0x08d, 0x1e2, 0x33b, 0x117, 0x26c, 0x3c5, 0x08e, 0x1e3, 0x33c,
0x118, 0x26d, 0x3c6, 0x08f, 0x1e4, 0x33d, 0x119, 0x26e, 0x3c7, 0x090, 0x1e5, 0x33e, 0x11a, 0x26f, 0x3c8, 0x091, 0x1e6,
0x33f, 0x11b, 0x270, 0x3c9, 0x092, 0x1e7, 0x340, 0x11c, 0x271, 0x3ca, 0x093, 0x1e8, 0x341, 0x11d, 0x272, 0x3cb, 0x094,
0x1e9, 0x342, 0x11e, 0x273, 0x3cc, 0x095, 0x1ea, 0x343, 0x11f, 0x274, 0x3cd, 0x096, 0x1eb, 0x344, 0x120, 0x275, 0x3ce,
0x097, 0x1ec, 0x345, 0x121, 0x276, 0x3cf, 0x098, 0x1ed, 0x346, 0x122, 0x277, 0x3d0, 0x099, 0x1ee, 0x347, 0x123, 0x278,
0x3d1, 0x09a, 0x1ef, 0x348, 0x124, 0x279, 0x3d2, 0x09b, 0x1f0, 0x349, 0x125, 0x27a, 0x3d3, 0x09c, 0x1f1, 0x34a, 0x126,
0x27b, 0x3d4, 0x09d, 0x1f2, 0x34b, 0x127, 0x27c, 0x3d5, 0x09e, 0x1f3, 0x34c, 0x128, 0x27d, 0x3d6, 0x09f, 0x1f4, 0x34d,
0x129, 0x27e, 0x3d7, 0x0a0, 0x1f5, 0x34e, 0x12a, 0x27f, 0x3d8, 0x0a1, 0x1f6, 0x34f, 0x12b, 0x280, 0x3d9, 0x0a2, 0x1f7,
0x350, 0x12c, 0x281, 0x3da, 0x0a3, 0x1f8, 0x351, 0x12d, 0x282, 0x3db, 0x0a4, 0x1f9, 0x352, 0x12e, 0x283, 0x3dc, 0x0a5,
0x1fa, 0x353, 0x12f, 0x284, 0x3dd, 0x0a6, 0x1fb, 0x354, 0x130, 0x285, 0x3de, 0x0a7, 0x1fc, 0x355, 0x131, 0x286, 0x3df,
0x0a8, 0x1fd, 0x356, 0x132,
];
//prettier-ignore: uint16, size 1024 (64x16)
const x3mScrambleTable = [
0x256, 0x28d, 0x213, 0x307, 0x156, 0x39d, 0x062, 0x170, 0x3ca, 0x035, 0x0ed, 0x2a4, 0x1e4, 0x359, 0x0d3, 0x26b, 0x265,
0x274, 0x251, 0x297, 0x202, 0x322, 0x126, 0x32b, 0x117, 0x302, 0x15c, 0x3a8, 0x057, 0x148, 0x380, 0x090, 0x1f6, 0x335,
0x10c, 0x2ee, 0x175, 0x3d4, 0x02b, 0x0cc, 0x260, 0x27b, 0x23d, 0x2bb, 0x1b6, 0x3a1, 0x05e, 0x157, 0x39e, 0x061, 0x16f,
0x3c6, 0x039, 0x0f7, 0x2b9, 0x1b8, 0x39f, 0x060, 0x166, 0x3b9, 0x046, 0x122, 0x31c, 0x12f, 0x33d, 0x0fc, 0x2ca, 0x1a4,
0x3cc, 0x033, 0x0e6, 0x293, 0x209, 0x315, 0x13d, 0x358, 0x0d5, 0x26e, 0x25e, 0x27d, 0x23a, 0x2c0, 0x1b1, 0x3af, 0x050,
0x136, 0x346, 0x0ef, 0x2aa, 0x1ce, 0x376, 0x0a0, 0x210, 0x30c, 0x14c, 0x389, 0x082, 0x1db, 0x367, 0x0b9, 0x23e, 0x2ba,
0x1b7, 0x3a0, 0x05f, 0x164, 0x3b7, 0x048, 0x125, 0x326, 0x11c, 0x30a, 0x14f, 0x38f, 0x070, 0x1a8, 0x3c7, 0x038, 0x0f5,
0x2b5, 0x1bd, 0x393, 0x06c, 0x199, 0x3e1, 0x01e, 0x0b3, 0x22f, 0x2d7, 0x193, 0x3ea, 0x015, 0x09d, 0x20a, 0x314, 0x13e,
0x35a, 0x0d2, 0x26a, 0x267, 0x272, 0x253, 0x294, 0x208, 0x319, 0x137, 0x34c, 0x0e7, 0x295, 0x205, 0x31d, 0x12e, 0x33c,
0x0fe, 0x2cd, 0x1a0, 0x3d5, 0x02a, 0x0c8, 0x258, 0x286, 0x22a, 0x2dc, 0x18e, 0x3f7, 0x008, 0x07c, 0x1d3, 0x370, 0x0a7,
0x21d, 0x2f1, 0x171, 0x3cd, 0x032, 0x0e5, 0x292, 0x20b, 0x313, 0x13f, 0x35c, 0x0d0, 0x266, 0x273, 0x252, 0x296, 0x204,
0x31f, 0x12a, 0x332, 0x10f, 0x2f4, 0x16c, 0x3c3, 0x03c, 0x101, 0x2d2, 0x19a, 0x3e0, 0x01f, 0x0b5, 0x233, 0x2c9, 0x1a6,
0x3c9, 0x036, 0x0f0, 0x2ab, 0x1cb, 0x37c, 0x095, 0x1fd, 0x328, 0x11a, 0x306, 0x158, 0x3a2, 0x05d, 0x155, 0x39c, 0x063,
0x174, 0x3d3, 0x02c, 0x0cf, 0x264, 0x275, 0x24f, 0x299, 0x1fa, 0x32c, 0x115, 0x2ff, 0x15f, 0x3ab, 0x054, 0x143, 0x36c,
0x0ad, 0x225, 0x2e5, 0x181, 0x3ef, 0x010, 0x08c, 0x1f1, 0x344, 0x0f3, 0x2af, 0x1c4, 0x386, 0x088, 0x1e3, 0x35b, 0x0d1,
0x269, 0x268, 0x26d, 0x25f, 0x27c, 0x23b, 0x2bf, 0x1b2, 0x3ae, 0x051, 0x13b, 0x355, 0x0da, 0x278, 0x248, 0x2a6, 0x1dc,
0x365, 0x0c0, 0x246, 0x2a8, 0x1d6, 0x36d, 0x0ac, 0x224, 0x2e8, 0x17e, 0x3eb, 0x014, 0x09c, 0x207, 0x31a, 0x133, 0x341,
0x0f8, 0x2bc, 0x1b5, 0x3a3, 0x05c, 0x152, 0x395, 0x06a, 0x18c, 0x3f9, 0x006, 0x07a, 0x1d1, 0x373, 0x0a4, 0x217, 0x2fe,
0x160, 0x3ad, 0x052, 0x13c, 0x357, 0x0d7, 0x270, 0x25c, 0x281, 0x235, 0x2c6, 0x1aa, 0x3bc, 0x043, 0x11d, 0x30d, 0x14a,
0x384, 0x08a, 0x1e7, 0x353, 0x0dd, 0x284, 0x22e, 0x2d8, 0x192, 0x3ec, 0x013, 0x099, 0x201, 0x323, 0x124, 0x321, 0x127,
0x32d, 0x114, 0x2fd, 0x161, 0x3b0, 0x04f, 0x135, 0x343, 0x0f4, 0x2b4, 0x1be, 0x392, 0x06d, 0x19d, 0x3db, 0x024, 0x0be,
0x244, 0x2b0, 0x1c2, 0x38a, 0x080, 0x1d9, 0x369, 0x0b6, 0x234, 0x2c8, 0x1a7, 0x3c8, 0x037, 0x0f1, 0x2ad, 0x1c6, 0x383,
0x08d, 0x1f2, 0x33b, 0x100, 0x2d1, 0x19b, 0x3de, 0x021, 0x0bb, 0x240, 0x2b6, 0x1bb, 0x399, 0x066, 0x17a, 0x3df, 0x020,
0x0b8, 0x23c, 0x2bd, 0x1b4, 0x3a5, 0x05a, 0x150, 0x390, 0x06f, 0x1a5, 0x3cb, 0x034, 0x0ea, 0x29d, 0x1ee, 0x348, 0x0ec,
0x2a3, 0x1e5, 0x356, 0x0d8, 0x271, 0x257, 0x289, 0x220, 0x2ec, 0x178, 0x3d9, 0x026, 0x0c2, 0x24b, 0x2a1, 0x1ea, 0x34d,
0x0e4, 0x291, 0x20c, 0x312, 0x141, 0x360, 0x0ca, 0x25a, 0x283, 0x230, 0x2d0, 0x19c, 0x3dd, 0x022, 0x0bc, 0x241, 0x2b3,
0x1bf, 0x391, 0x06e, 0x1a2, 0x3d1, 0x02e, 0x0d6, 0x26f, 0x25d, 0x27f, 0x237, 0x2c4, 0x1ac, 0x3ba, 0x045, 0x121, 0x318,
0x138, 0x34e, 0x0e3, 0x28f, 0x211, 0x30b, 0x14d, 0x38c, 0x073, 0x1c3, 0x387, 0x084, 0x1df, 0x362, 0x0c7, 0x255, 0x28e,
0x212, 0x309, 0x153, 0x396, 0x069, 0x18b, 0x3fa, 0x005, 0x079, 0x1d0, 0x374, 0x0a2, 0x215, 0x301, 0x15d, 0x3a9, 0x056,
0x147, 0x37a, 0x098, 0x200, 0x324, 0x11f, 0x316, 0x13a, 0x352, 0x0df, 0x288, 0x223, 0x2e9, 0x17d, 0x3e9, 0x016, 0x09e,
0x20d, 0x310, 0x144, 0x372, 0x0a5, 0x219, 0x2fa, 0x165, 0x3b8, 0x047, 0x123, 0x31e, 0x12d, 0x338, 0x107, 0x2e0, 0x188,
0x3fe, 0x001, 0x075, 0x1c9, 0x37e, 0x093, 0x1f9, 0x32f, 0x112, 0x2f9, 0x167, 0x3be, 0x041, 0x10b, 0x2e7, 0x17f, 0x3ed,
0x012, 0x097, 0x1ff, 0x325, 0x11e, 0x311, 0x142, 0x366, 0x0ba, 0x23f, 0x2b8, 0x1b9, 0x39b, 0x064, 0x176, 0x3d6, 0x029,
0x0c5, 0x250, 0x298, 0x1fc, 0x329, 0x119, 0x304, 0x15a, 0x3a6, 0x059, 0x14e, 0x38e, 0x071, 0x1ad, 0x3b6, 0x049, 0x128,
0x32e, 0x113, 0x2fc, 0x162, 0x3b2, 0x04d, 0x131, 0x33f, 0x0fa, 0x2c2, 0x1af, 0x3b3, 0x04c, 0x130, 0x33e, 0x0fb, 0x2c7,
0x1a9, 0x3bd, 0x042, 0x116, 0x300, 0x15e, 0x3aa, 0x055, 0x146, 0x378, 0x09b, 0x206, 0x31b, 0x132, 0x340, 0x0f9, 0x2be,
0x1b3, 0x3ac, 0x053, 0x140, 0x35d, 0x0ce, 0x262, 0x279, 0x247, 0x2a7, 0x1d7, 0x36b, 0x0ae, 0x226, 0x2e3, 0x185, 0x3f6,
0x009, 0x07d, 0x1d4, 0x36f, 0x0a8, 0x21e, 0x2f0, 0x172, 0x3ce, 0x031, 0x0de, 0x287, 0x228, 0x2df, 0x189, 0x3fd, 0x002,
0x076, 0x1ca, 0x37d, 0x094, 0x1fb, 0x32a, 0x118, 0x303, 0x15b, 0x3a7, 0x058, 0x14b, 0x388, 0x083, 0x1dd, 0x364, 0x0c1,
0x24a, 0x2a2, 0x1e9, 0x350, 0x0e1, 0x28b, 0x21a, 0x2f8, 0x168, 0x3bf, 0x040, 0x10a, 0x2e6, 0x180, 0x3ee, 0x011, 0x091,
0x1f7, 0x334, 0x10d, 0x2ef, 0x173, 0x3cf, 0x030, 0x0dc, 0x280, 0x236, 0x2c5, 0x1ab, 0x3bb, 0x044, 0x120, 0x317, 0x139,
0x34f, 0x0e2, 0x28c, 0x218, 0x2fb, 0x163, 0x3b4, 0x04b, 0x12c, 0x337, 0x108, 0x2e2, 0x186, 0x3fc, 0x003, 0x077, 0x1cc,
0x37b, 0x096, 0x1fe, 0x327, 0x11b, 0x308, 0x154, 0x397, 0x068, 0x183, 0x3f3, 0x00c, 0x085, 0x1e0, 0x361, 0x0c9, 0x259,
0x285, 0x22c, 0x2da, 0x190, 0x3f2, 0x00d, 0x086, 0x1e1, 0x35f, 0x0cb, 0x25b, 0x282, 0x232, 0x2cc, 0x1a1, 0x3d2, 0x02d,
0x0d4, 0x26c, 0x263, 0x277, 0x249, 0x2a5, 0x1de, 0x363, 0x0c6, 0x254, 0x290, 0x20e, 0x30f, 0x145, 0x377, 0x09f, 0x20f,
0x30e, 0x149, 0x382, 0x08e, 0x1f3, 0x33a, 0x102, 0x2d3, 0x198, 0x3e2, 0x01d, 0x0b2, 0x22d, 0x2d9, 0x191, 0x3f1, 0x00e,
0x087, 0x1e2, 0x35e, 0x0cd, 0x261, 0x27a, 0x243, 0x2b1, 0x1c1, 0x38b, 0x07f, 0x1d8, 0x36a, 0x0b4, 0x231, 0x2cf, 0x19e,
0x3da, 0x025, 0x0bf, 0x245, 0x2ac, 0x1c7, 0x381, 0x08f, 0x1f5, 0x336, 0x109, 0x2e4, 0x184, 0x3f4, 0x00b, 0x081, 0x1da,
0x368, 0x0b7, 0x239, 0x2c1, 0x1b0, 0x3b1, 0x04e, 0x134, 0x342, 0x0f6, 0x2b7, 0x1ba, 0x39a, 0x065, 0x179, 0x3dc, 0x023,
0x0bd, 0x242, 0x2b2, 0x1c0, 0x38d, 0x072, 0x1bc, 0x398, 0x067, 0x182, 0x3f0, 0x00f, 0x08b, 0x1e8, 0x351, 0x0e0, 0x28a,
0x21c, 0x2f3, 0x16d, 0x3c4, 0x03b, 0x0ff, 0x2ce, 0x19f, 0x3d7, 0x028, 0x0c4, 0x24e, 0x29b, 0x1f0, 0x345, 0x0f2, 0x2ae,
0x1c5, 0x385, 0x089, 0x1e6, 0x354, 0x0db, 0x27e, 0x238, 0x2c3, 0x1ae, 0x3b5, 0x04a, 0x12b, 0x333, 0x10e, 0x2f2, 0x16e,
0x3c5, 0x03a, 0x0fd, 0x2cb, 0x1a3, 0x3d0, 0x02f, 0x0d9, 0x276, 0x24c, 0x29f, 0x1ec, 0x34a, 0x0e9, 0x29c, 0x1ef, 0x347,
0x0ee, 0x2a9, 0x1cf, 0x375, 0x0a1, 0x214, 0x305, 0x159, 0x3a4, 0x05b, 0x151, 0x394, 0x06b, 0x196, 0x3e6, 0x019, 0x0ab,
0x222, 0x2ea, 0x17c, 0x3e5, 0x01a, 0x0af, 0x227, 0x2e1, 0x187, 0x3ff, 0x000, 0x074, 0x1c8, 0x37f, 0x092, 0x1f8, 0x331,
0x110, 0x2f6, 0x16a, 0x3c1, 0x03e, 0x104, 0x2d5, 0x195, 0x3e7, 0x018, 0x0aa, 0x221, 0x2eb, 0x17b, 0x3e4, 0x01b, 0x0b0,
0x229, 0x2dd, 0x18d, 0x3f8, 0x007, 0x07b, 0x1d2, 0x371, 0x0a6, 0x21b, 0x2f5, 0x16b, 0x3c2, 0x03d, 0x103, 0x2d4, 0x197,
0x3e3, 0x01c, 0x0b1, 0x22b, 0x2db, 0x18f, 0x3f5, 0x00a, 0x07e, 0x1d5, 0x36e, 0x0a9, 0x21f, 0x2ed, 0x177, 0x3d8, 0x027,
0x0c3, 0x24d, 0x29e, 0x1ed, 0x349, 0x0eb, 0x2a0, 0x1eb, 0x34b, 0x0e8, 0x29a, 0x1f4, 0x339, 0x106, 0x2de, 0x18a, 0x3fb,
0x004, 0x078, 0x1cd, 0x379, 0x09a, 0x203, 0x320, 0x129, 0x330, 0x111, 0x2f7, 0x169, 0x3c0, 0x03f, 0x105, 0x2d6, 0x194,
0x3e8, 0x017, 0x0a3, 0x216,
];

View File

@ -1,65 +0,0 @@
import {Decrypt as RawDecrypt} from "./raw";
import {AudioMimeType, BytesHasPrefix, GetArrayBuffer, GetCoverFromFile, GetMetaFromFile} from "@/decrypt/utils.ts";
import {parseBlob as metaParseBlob} from "music-metadata-browser";
const MagicHeader = [0x69, 0x66, 0x6D, 0x74]
const MagicHeader2 = [0xfe, 0xfe, 0xfe, 0xfe]
const FileTypeMap = {
" WAV": ".wav",
"FLAC": ".flac",
" MP3": ".mp3",
" A4M": ".m4a",
}
export async function Decrypt(file, raw_filename, raw_ext) {
const oriData = new Uint8Array(await GetArrayBuffer(file));
if (!BytesHasPrefix(oriData, MagicHeader) || !BytesHasPrefix(oriData.slice(8, 12), MagicHeader2)) {
if (raw_ext === "xm") {
return {status: false, message: "此xm文件已损坏"}
} else {
return await RawDecrypt(file, raw_filename, raw_ext, true)
}
}
let typeText = (new TextDecoder()).decode(oriData.slice(4, 8))
if (!FileTypeMap.hasOwnProperty(typeText)) {
return {status: false, message: "未知的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;
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.log(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 {
status: true,
title,
artist,
ext,
mime,
album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta),
file: URL.createObjectURL(musicBlob),
rawExt: "xm"
}
}

67
src/decrypt/xm.ts Normal file
View File

@ -0,0 +1,67 @@
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';
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',
};
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
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 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 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,
String(musicMeta.common.artists || 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',
};
}

View File

@ -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));

View File

@ -1,25 +0,0 @@
//TODO: Use other method to fix this
// !! Only Temporary Solution
// it seems like that @babel/plugin-proposal-object-rest-spread not working
// to fix up the compatibility for Edge 18 and some older Chromium
// now manually edit the dependency files
const fs = require('fs');
const filePath = "./node_modules/file-type/core.js";
const regReplace = /{\s*([a-zA-Z0-9:,\s]*),\s*\.\.\.([a-zA-Z0-9]*)\s*};/m;
if (fs.existsSync(filePath)) {
console.log("File Found!");
let data = fs.readFileSync(filePath).toString();
const regResult = regReplace.exec(data);
if (regResult != null) {
data = data.replace(regResult[0],
"Object.assign({ " + regResult[1] + " }, " + regResult[2] + ");"
);
fs.writeFileSync(filePath, data);
console.log("Object rest spread in file-type fixed!");
} else {
console.log("No fix needed.");
}
} else {
console.log("File Not Found!");
}

View File

@ -1,30 +1,39 @@
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
Button,
Checkbox,
Col,
Container,
Dialog,
Form,
FormItem,
Footer,
Icon,
Image,
Input,
Link,
Main,
Notification,
Progress,
Radio,
Row,
Table,
TableColumn,
Tooltip,
Upload,
MessageBox,
} from 'element-ui';
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);
@ -39,8 +48,9 @@ Vue.use(Radio);
Vue.use(Tooltip);
Vue.use(Progress);
Vue.prototype.$notify = Notification;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
render: (h) => h(App),
}).$mount('#app');

View File

@ -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);
},
});
}

View File

@ -1,7 +1,5 @@
/*
* name: 样式 - 夜间模式
* author: @KyleBing
* date: 2020-11-24
* 样式 - 暗黑模式
*/
@media (prefers-color-scheme: dark) {
@ -37,9 +35,11 @@
.el-checkbox.is-bordered{
border-color: $dark-border;
color: $dark-text-main;
background-color: $dark-btn-bg;
.el-checkbox__inner{
background-color: $dark-btn-bg;
border-color: $dark-border;
background-color: $dark-btn-bg-highlight;
border-color: $dark-border-highlight;
}
&:hover{
border-color: $dark-border-highlight;
@ -54,12 +54,18 @@
&.is-checked{
background-color: $blue;
.el-checkbox__inner{
border-color: $dark-btn-bg-highlight;
border-color: white;
}
.el-checkbox__label{
color: white;
font-weight: bold;
}
&:hover{
border-color: $blue;
.el-checkbox__inner{
background-color: white;
}
}
}
}
@ -84,6 +90,35 @@
color: white;
}
}
&.is-circle {
background-color: $dark-blue;
border-color: $dark-blue;
&:hover {
background-color: $blue;
border-color: $blue;
color: white;
}
}
}
&--success{
&.is-plain {
background-color: $dark-btn-bg;
&:hover {
background-color: $green;
border-color: $green;
color: white;
}
}
&.is-circle {
background-color: $dark-green;
border-color: $dark-green;
&:hover {
background-color: $green;
border-color: $green;
color: white;
}
}
}
&--danger{
&.is-plain{
@ -94,6 +129,15 @@
border-color: $red;
}
}
&.is-circle {
background-color: $dark-red;
border-color: $dark-red;
&:hover {
background-color: $red;
border-color: $red;
color: white;
}
}
}
}
@ -113,7 +157,7 @@
}
}
//TABLE
// TABLE
.el-table{
background-color: $dark-bg-td;
&:before{ // 去除表格末尾的横线
@ -124,7 +168,7 @@
border-bottom-color: $dark-border !important;
}
}
th{
th.el-table__cell{
background-color: $dark-bg-th;
color: $dark-text-info;
}
@ -142,7 +186,6 @@
}
}
// LINKS
a{
text-decoration: none;
@ -163,4 +206,51 @@
color: $dark-text-info;
}
}
// DIALOG
.el-dialog{
background-color: $dark-dialog-bg;
.el-dialog__header{
.el-dialog__title{
color: $dark-text-main;
}
}
.el-dialog__body{
color: $dark-text-main;
.el-input{
.el-input__inner{
color: $dark-text-main;
background-color: $dark-btn-bg;
}
.el-input__suffix{
.el-input__suffix-inner{
}
}
.el-input__count{
.el-input__count-inner{
background-color: transparent;
}
}
}
}
.item-desc{
color: $dark-text-info;
}
}
// 自定义样式
// 首页弹窗提示信息的 更新信息 面板
.update-info{
border: 1px solid $dark-btn-bg !important;
.update-title{
color: $dark-text-main;
background-color: $dark-btn-bg !important;
}
.update-content{
color: $dark-text-info;
padding: 10px;
}
}
}

View File

@ -0,0 +1,39 @@
$color-checkbox: $blue;
$color-border-el: #DCDFE6;
$btn-radius: 6px;
/* FORM */
// checkbox
.el-checkbox.is-bordered{
@include border-radius($btn-radius) ;
&:hover{
border-color: $color-checkbox;
.el-checkbox__label{
color: $color-checkbox;
}
}
.el-checkbox__input.is-focus{
.el-checkbox__inner{
border-color: $color-border-el;
}
}
&.is-checked{
background-color: $color-checkbox;
.el-checkbox__label{
color: white;
}
.el-checkbox__inner{
border-color: white;
background-color: white;
&:after{
border-color: $color-checkbox;
}
}
}
}
// el-button
.el-button{
@include border-radius($btn-radius) ;
}

View File

@ -3,7 +3,7 @@
*/
$gap: 5px;
@for $item from 1 through 7 {
@for $item from 0 through 8 {
.mt-#{$item} { margin-top : $gap * $item !important;}
.mb-#{$item} { margin-bottom : $gap * $item !important;}
.ml-#{$item} { margin-left : $gap * $item !important;}
@ -15,4 +15,4 @@ $gap: 5px;
.pl-#{$item} { padding-left : $gap * $item !important;}
.pr-#{$item} { padding-right : $gap * $item !important;}
.p-#{$item} { padding : $gap * $item !important;}
}
}

72
src/scss/_utility.scss Normal file
View File

@ -0,0 +1,72 @@
// box-shadow
@mixin box-shadow($value...){
-webkit-box-shadow: $value;
-moz-box-shadow: $value;
box-shadow: $value;
}
// border-radius
@mixin border-radius($corner...){
-webkit-border-radius: $corner;
-moz-border-radius: $corner;
border-radius: $corner;
}
@mixin clearfix(){
&:after{
content: '';
display: block;
clear: both;
visibility: hidden;
}
}
@mixin transform($value){
-webkit-transform: $value;
-moz-transform: $value;
-ms-transform: $value;
-o-transform: $value;
transform: $value;
}
@mixin transition($value...){
-webkit-transition: $value;
-moz-transition: $value;
-ms-transition: $value;
-o-transition: $value;
transition: $value;
}
@mixin animation($value){
animation: $value;
-webkit-animation: $value;
}
@mixin linear-gradient($direct, $colors){
background: linear-gradient($direct, $colors);
background: -webkit-linear-gradient($direct, $colors);
background: -moz-linear-gradient($direct, $colors);
}
@mixin backdrop-filter($value){
backdrop-filter: $value ;
-webkit-backdrop-filter: $value;
}
/*
Extension
*/
.unselectable {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.btn-like{
&:active{
@include transform(translateY(2px))
}
}

View File

@ -8,6 +8,8 @@ $text-main : #2C3E50;
$color-link: $blue;
$fz-main: 14px;
$fz-mini-title: 13px;
$fz-mini-content: 12px;
$font-family: "Helvetica Neue", Helvetica, "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
@ -20,9 +22,14 @@ $dark-bg : lighten(black, 10%);
$dark-text-main : lighten(black, 90%);
$dark-text-info : lighten(black, 60%);
$dark-uploader-bg : lighten(black, 13%);
$dark-dialog-bg : lighten(black, 15%);
$dark-uploader-bg-highlight : lighten(black, 18%);
$dark-btn-bg : lighten(black, 20%);
$dark-btn-bg-highlight : lighten(black, 30%);
$dark-bg-th : lighten(black, 18%);
$dark-bg-td : lighten(black, 13%);
$dark-color-link : $green;
$dark-color-link : $green;
$dark-blue : darken(desaturate($blue, 40%), 30%);
$dark-red : darken(desaturate($red, 50%), 30%);
$dark-green : darken(desaturate($green, 30%), 30%);

View File

@ -1,5 +1,26 @@
@import "variables";
@import "utility";
@import "gaps";
@import "element-ui-overrite";
@import "normal";
@import "dark-mode"; // dark-mode 放在 normal 后面以获得更高优先级
@import "dark-mode"; // dark-mode 放在 normal 后面以获得更高优先级
// 首页弹窗提示信息的 更新信息 面板
.update-info{
@include border-radius(8px);
overflow: hidden;
border: 1px solid $color-border-el;
margin: 10px 0;
.update-title{
font-size: $fz-mini-title;
padding: 5px 10px;
background-color: $color-border-el;
}
.update-content{
font-size: $fz-mini-content;
line-height: 1.5;
padding: 10px;
}
}

23
src/shims-browser-id3-writer.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
declare module 'browser-id3-writer' {
export default class ID3Writer {
constructor(buffer: Buffer | ArrayBuffer);
setFrame(name: string, value: string | object | string[]);
addTag(): Uint8Array;
}
}
declare module 'metaflac-js' {
export default class Metaflac {
constructor(buffer: Buffer);
setTag(field: string);
removeTag(name: string);
importPictureFromBuffer(picture: Buffer);
save(): Buffer;
}
}

54
src/shims-fs.d.ts vendored Normal file
View File

@ -0,0 +1,54 @@
export interface FileSystemGetFileOptions {
create?: boolean;
}
interface FileSystemCreateWritableOptions {
keepExistingData?: boolean;
}
interface FileSystemRemoveOptions {
recursive?: boolean;
}
interface FileSystemFileHandle {
getFile(): Promise<File>;
createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>;
}
enum WriteCommandType {
write = 'write',
seek = 'seek',
truncate = 'truncate',
}
interface WriteParams {
type: WriteCommandType;
size?: number;
position?: number;
data: BufferSource | Blob | string;
}
type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams;
interface FileSystemWritableFileStream extends WritableStream {
write(data: FileSystemWriteChunkType): Promise<undefined>;
seek(position: number): Promise<undefined>;
truncate(size: number): Promise<undefined>;
close(): Promise<undefined>; // should be implemented in WritableStream
}
export declare interface FileSystemDirectoryHandle {
getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>;
removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<undefined>;
}
declare global {
interface Window {
showDirectoryPicker?(): Promise<FileSystemDirectoryHandle>;
}
}

10
src/shims-tsx.d.ts vendored
View File

@ -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;
}
}
}

4
src/shims-vue.d.ts vendored
View File

@ -1,4 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
import Vue from 'vue';
export default Vue;
}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export const extractQQMusicMeta = jest.fn();

View File

@ -0,0 +1,4 @@
export const storage = {
loadJooxUUID: jest.fn(),
saveJooxUUID: jest.fn(),
};

Some files were not shown because too many files have changed in this diff Show More