Browse Source

Merge pull request 'Add Tag Edit Function & Wasm for Qmc & Kgm' (#5) from xhacker-zzz/web:master into master

Reviewed-on: https://git.unlock-music.dev/um/web/pulls/5
20230320
um-dev 2 years ago
parent
commit
4d3ef0ddaa
  1. 3
      .github/workflows/build.yml
  2. 3
      .github/workflows/release-build.yml
  3. 7
      .gitignore
  4. 14
      README.md
  5. 11
      build-wasm
  6. 31
      package-lock.json
  7. 5
      package.json
  8. 65
      src/KgmWasm/CMakeLists.txt
  9. 20
      src/KgmWasm/KgmWasm.cpp
  10. 18
      src/KgmWasm/KgmWasm.h
  11. 9
      src/KgmWasm/README.md
  12. 41
      src/KgmWasm/build-wasm
  13. 112
      src/KgmWasm/kgm.hpp
  14. 65
      src/QmcWasm/CMakeLists.txt
  15. 57
      src/QmcWasm/QmcWasm.cpp
  16. 23
      src/QmcWasm/QmcWasm.h
  17. 9
      src/QmcWasm/README.md
  18. 289
      src/QmcWasm/TencentTea.hpp
  19. 207
      src/QmcWasm/base64.hpp
  20. 41
      src/QmcWasm/build-wasm
  21. 230
      src/QmcWasm/qmc.hpp
  22. 290
      src/QmcWasm/qmc_cipher.hpp
  23. 217
      src/QmcWasm/qmc_key.hpp
  24. 178
      src/component/EditDialog.vue
  25. 4
      src/component/PreviewTable.vue
  26. 4
      src/decrypt/index.ts
  27. 41
      src/decrypt/kgm.ts
  28. 67
      src/decrypt/kgm_wasm.ts
  29. 2
      src/decrypt/kwm.ts
  30. 71
      src/decrypt/mg3d.ts
  31. 2
      src/decrypt/ncmcache.ts
  32. 15
      src/decrypt/qmc.ts
  33. 26
      src/decrypt/qmc_key.ts
  34. 100
      src/decrypt/qmc_wasm.ts
  35. 41
      src/decrypt/qmccache.ts
  36. 2
      src/decrypt/raw.ts
  37. 88
      src/decrypt/utils.ts
  38. 2
      src/decrypt/xm.ts
  39. 23
      src/utils/qm_meta.ts
  40. 87
      src/view/Home.vue

3
.github/workflows/build.yml

@ -63,6 +63,9 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Build Wasm
run: bash build-wasm
- name: Build - name: Build
run: npm run build ${{ matrix.BUILD_ARGS }} run: npm run build ${{ matrix.BUILD_ARGS }}

3
.github/workflows/release-build.yml

@ -19,6 +19,9 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
- name: Build Wasm
run: bash build-wasm
- name: Build Legacy - name: Build Legacy
env: env:
GZIP: "--best" GZIP: "--best"

7
.gitignore

@ -20,3 +20,10 @@ yarn-error.log*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
/src/KgmWasm/build
/src/KgmWasm/*.js
/src/KgmWasm/*.wasm
/src/QmcWasm/build
/src/QmcWasm/*.js
/src/QmcWasm/*.wasm

14
README.md

@ -67,6 +67,8 @@ docker run --name unlock-music -d -p 8080:80 ix64/unlock-music
### 自行构建 ### 自行构建
#### JS部分
- 环境要求 - 环境要求
- nodejs (v16.x) - nodejs (v16.x)
- npm - npm
@ -90,3 +92,15 @@ docker run --name unlock-music -d -p 8080:80 ix64/unlock-music
```sh ```sh
npm run make-extension npm run make-extension
``` ```
#### WASM部分
- 环境要求
- Linux
- python3
- 运行此目录下的build-wasm
```sh
bash build-wasm
```

11
build-wasm

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
pushd ./src/QmcWasm
bash build-wasm
popd
pushd ./src/QmcWasm
bash build-wasm
popd

31
package-lock.json

@ -1,18 +1,17 @@
{ {
"name": "unlock-music", "name": "unlock-music",
"version": "v1.10.0", "version": "v1.10.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "unlock-music", "name": "unlock-music",
"version": "v1.10.0", "version": "v1.10.3",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/preset-typescript": "^7.16.5", "@babel/preset-typescript": "^7.16.5",
"@jixun/kugou-crypto": "^1.0.3", "@jixun/kugou-crypto": "^1.0.3",
"@jixun/qmc2-crypto": "^0.0.6-R1",
"@unlock-music/joox-crypto": "^0.0.1-R5", "@unlock-music/joox-crypto": "^0.0.1-R5",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"browser-id3-writer": "^4.4.0", "browser-id3-writer": "^4.4.0",
@ -3003,11 +3002,6 @@
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
}, },
"node_modules/@jixun/qmc2-crypto": {
"version": "0.0.6-R1",
"resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.6-R1.tgz",
"integrity": "sha512-G7oa28/tGozJIIkF2DS7RWewoDsKrmGM5JgthzCfB6P1psfCjpjwH21RhnY9RzNlfdGZBqyWkAKwXMiUx/xhNA=="
},
"node_modules/@mrmlnc/readdir-enhanced": { "node_modules/@mrmlnc/readdir-enhanced": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@ -5722,13 +5716,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001298", "version": "1.0.30001434",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
"integrity": "sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ==", "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA=="
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
}
}, },
"node_modules/case-sensitive-paths-webpack-plugin": { "node_modules/case-sensitive-paths-webpack-plugin": {
"version": "2.4.0", "version": "2.4.0",
@ -23220,11 +23210,6 @@
} }
} }
}, },
"@jixun/qmc2-crypto": {
"version": "0.0.6-R1",
"resolved": "https://registry.npmjs.org/@jixun/qmc2-crypto/-/qmc2-crypto-0.0.6-R1.tgz",
"integrity": "sha512-G7oa28/tGozJIIkF2DS7RWewoDsKrmGM5JgthzCfB6P1psfCjpjwH21RhnY9RzNlfdGZBqyWkAKwXMiUx/xhNA=="
},
"@mrmlnc/readdir-enhanced": { "@mrmlnc/readdir-enhanced": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@ -25501,9 +25486,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001298", "version": "1.0.30001434",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
"integrity": "sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ==" "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA=="
}, },
"case-sensitive-paths-webpack-plugin": { "case-sensitive-paths-webpack-plugin": {
"version": "2.4.0", "version": "2.4.0",

5
package.json

@ -1,8 +1,8 @@
{ {
"name": "unlock-music", "name": "unlock-music",
"version": "v1.10.0", "version": "v1.10.3",
"ext_build": 0, "ext_build": 0,
"updateInfo": "重写QMC解锁,完全支持.mflac*/.mgg*; 支持JOOX解锁", "updateInfo": "完善音乐标签编辑功能,支持编辑更多标签",
"license": "MIT", "license": "MIT",
"description": "Unlock encrypted music file in browser.", "description": "Unlock encrypted music file in browser.",
"repository": { "repository": {
@ -22,7 +22,6 @@
"dependencies": { "dependencies": {
"@babel/preset-typescript": "^7.16.5", "@babel/preset-typescript": "^7.16.5",
"@jixun/kugou-crypto": "^1.0.3", "@jixun/kugou-crypto": "^1.0.3",
"@jixun/qmc2-crypto": "^0.0.6-R1",
"@unlock-music/joox-crypto": "^0.0.1-R5", "@unlock-music/joox-crypto": "^0.0.1-R5",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"browser-id3-writer": "^4.4.0", "browser-id3-writer": "^4.4.0",

65
src/KgmWasm/CMakeLists.txt

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

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

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

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

@ -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
mkdir -p build/wasm
if [ ! -d build/emsdk ]; then
git clone https://github.com/emscripten-core/emsdk.git build/emsdk
fi
pushd build/emsdk
#git pull
./emsdk install 3.0.0
./emsdk activate 3.0.0
source ./emsdk_env.sh
popd # build/emsdk
pushd build/wasm
emcmake cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" ../..
make -j
TARGET_FILES="
KgmLegacy.js
KgmWasm.js
KgmWasm.wasm
KgmWasmBundle.js
"
#mkdir -p "${CURR_DIR}/npm"
#cp $TARGET_FILES "${CURR_DIR}/npm/"
cp $TARGET_FILES "${CURR_DIR}/"
popd # build/wasm
popd

112
src/KgmWasm/kgm.hpp

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

65
src/QmcWasm/CMakeLists.txt

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

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

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

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

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

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

@ -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
mkdir -p build/wasm
if [ ! -d build/emsdk ]; then
git clone https://github.com/emscripten-core/emsdk.git build/emsdk
fi
pushd build/emsdk
#git pull
./emsdk install 3.0.0
./emsdk activate 3.0.0
source ./emsdk_env.sh
popd # build/emsdk
pushd build/wasm
emcmake cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" ../..
make -j
TARGET_FILES="
QmcLegacy.js
QmcWasm.js
QmcWasm.wasm
QmcWasmBundle.js
"
#mkdir -p "${CURR_DIR}/npm"
#cp $TARGET_FILES "${CURR_DIR}/npm/"
cp $TARGET_FILES "${CURR_DIR}/"
popd # build/wasm
popd

230
src/QmcWasm/qmc.hpp

@ -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 == "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;
}
if (cipherType == "invalid") {
error = "file is invalid or not supported(Please downgrade your app.)";
return -1;
}
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

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

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

178
src/component/EditDialog.vue

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

4
src/component/PreviewTable.vue

@ -27,6 +27,7 @@
<el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)"> <el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
</el-button> </el-button>
<el-button circle icon="el-icon-download" @click="handleDownload(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 circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
</el-button> </el-button>
</template> </template>
@ -55,6 +56,9 @@ export default {
handleDownload(row) { handleDownload(row) {
this.$emit('download', row); this.$emit('download', row);
}, },
handleEdit(row) {
this.$emit('edit', row);
},
}, },
}; };
</script> </script>

4
src/decrypt/index.ts

@ -1,3 +1,4 @@
import { Decrypt as Mg3dDecrypt } from '@/decrypt/mg3d';
import { Decrypt as NcmDecrypt } from '@/decrypt/ncm'; import { Decrypt as NcmDecrypt } from '@/decrypt/ncm';
import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache'; import { Decrypt as NcmCacheDecrypt } from '@/decrypt/ncmcache';
import { Decrypt as XmDecrypt } from '@/decrypt/xm'; import { Decrypt as XmDecrypt } from '@/decrypt/xm';
@ -22,6 +23,9 @@ export async function Decrypt(file: FileInfo, config: Record<string, any>): Prom
const raw = SplitFilename(file.name); const raw = SplitFilename(file.name);
let rt_data: DecryptResult; let rt_data: DecryptResult;
switch (raw.ext) { switch (raw.ext) {
case 'mg3d': // Migu Wav
rt_data = await Mg3dDecrypt(file.raw, raw.name);
break;
case 'ncm': // Netease Mp3/Flac case 'ncm': // Netease Mp3/Flac
rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext); rt_data = await NcmDecrypt(file.raw, raw.name, raw.ext);
break; break;

41
src/decrypt/kgm.ts

@ -8,6 +8,7 @@ import {
} from '@/decrypt/utils'; } from '@/decrypt/utils';
import { parseBlob as metaParseBlob } from 'music-metadata-browser'; import { parseBlob as metaParseBlob } from 'music-metadata-browser';
import { DecryptResult } from '@/decrypt/entity'; import { DecryptResult } from '@/decrypt/entity';
import { DecryptKgmWasm } from '@/decrypt/kgm_wasm';
import { decryptKgmByteAtOffsetV2, decryptVprByteAtOffset } from '@jixun/kugou-crypto/dist/utils/decryptionHelper'; import { decryptKgmByteAtOffsetV2, decryptVprByteAtOffset } from '@jixun/kugou-crypto/dist/utils/decryptionHelper';
//prettier-ignore //prettier-ignore
@ -22,31 +23,47 @@ const KgmHeader = [
] ]
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> { export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
const oriData = new Uint8Array(await GetArrayBuffer(file)); const oriData = await GetArrayBuffer(file);
if (raw_ext === 'vpr') { if (raw_ext === 'vpr') {
if (!BytesHasPrefix(oriData, VprHeader)) throw Error('Not a valid vpr file!'); if (!BytesHasPrefix(new Uint8Array(oriData), VprHeader)) throw Error('Not a valid vpr file!');
} else { } else {
if (!BytesHasPrefix(oriData, KgmHeader)) throw Error('Not a valid kgm(a) file!'); 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)');
}
} }
let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer);
let headerLen = bHeaderLen.getUint32(0, true);
let audioData = oriData.slice(headerLen); if (!musicDecoded) {
let dataLen = audioData.length; musicDecoded = new Uint8Array(oriData);
let bHeaderLen = new DataView(musicDecoded.slice(0x10, 0x14).buffer);
let headerLen = bHeaderLen.getUint32(0, true);
let key1 = Array.from(oriData.slice(0x1c, 0x2c)); let key1 = Array.from(musicDecoded.slice(0x1c, 0x2c));
key1.push(0); key1.push(0);
musicDecoded = musicDecoded.slice(headerLen);
let dataLen = musicDecoded.length;
const decryptByte = raw_ext === 'vpr' ? decryptVprByteAtOffset : decryptKgmByteAtOffsetV2; const decryptByte = raw_ext === 'vpr' ? decryptVprByteAtOffset : decryptKgmByteAtOffsetV2;
for (let i = 0; i < dataLen; i++) { for (let i = 0; i < dataLen; i++) {
audioData[i] = decryptByte(audioData[i], key1, i); musicDecoded[i] = decryptByte(musicDecoded[i], key1, i);
}
} }
const ext = SniffAudioExt(audioData); const ext = SniffAudioExt(musicDecoded);
const mime = AudioMimeType[ext]; const mime = AudioMimeType[ext];
let musicBlob = new Blob([audioData], { type: mime }); let musicBlob = new Blob([musicDecoded], { type: mime });
const musicMeta = await metaParseBlob(musicBlob); const musicMeta = await metaParseBlob(musicBlob);
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist); const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || ""));
return { return {
album: musicMeta.common.album, album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta), picture: GetCoverFromFile(musicMeta),

67
src/decrypt/kgm_wasm.ts

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

2
src/decrypt/kwm.ts

@ -38,7 +38,7 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom
let musicBlob = new Blob([audioData], { type: mime }); let musicBlob = new Blob([audioData], { type: mime });
const musicMeta = await metaParseBlob(musicBlob); const musicMeta = await metaParseBlob(musicBlob);
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist); const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || ""));
return { return {
album: musicMeta.common.album, album: musicMeta.common.album,
picture: GetCoverFromFile(musicMeta), picture: GetCoverFromFile(musicMeta),

71
src/decrypt/mg3d.ts

@ -0,0 +1,71 @@
import { Decrypt as RawDecrypt } from './raw';
import { GetArrayBuffer } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity';
const segmentSize = 0x20;
function isPrintableAsciiChar(ch: number) {
return ch >= 0x20 && ch <= 0x7E;
}
function isUpperHexChar(ch: number) {
return (ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x46);
}
/**
* @param {Buffer} data
* @param {Buffer} key
* @param {boolean} copy
* @returns Buffer
*/
function decryptSegment(data: Uint8Array, key: Uint8Array) {
for (let i = 0; i < data.byteLength; i++) {
data[i] -= key[i % segmentSize];
}
return Buffer.from(data);
}
export async function Decrypt(file: File, raw_filename: string): Promise<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);
}

2
src/decrypt/ncmcache.ts

@ -13,7 +13,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
const ext = SniffAudioExt(buffer, raw_ext); const ext = SniffAudioExt(buffer, raw_ext);
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] }); if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
const tag = await metaParseBlob(file); const tag = await metaParseBlob(file);
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist); const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ""));
return { return {
title, title,

15
src/decrypt/qmc.ts

@ -3,7 +3,7 @@ import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
import { DecryptResult } from '@/decrypt/entity'; import { DecryptResult } from '@/decrypt/entity';
import { QmcDeriveKey } from '@/decrypt/qmc_key'; import { QmcDeriveKey } from '@/decrypt/qmc_key';
import { DecryptQMCWasm } from '@/decrypt/qmc_wasm'; import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
import { extractQQMusicMeta } from '@/utils/qm_meta'; import { extractQQMusicMeta } from '@/utils/qm_meta';
interface Handler { interface Handler {
@ -24,9 +24,9 @@ export const HandlerMap: { [key: string]: Handler } = {
qmcflac: { ext: 'flac', version: 2 }, qmcflac: { ext: 'flac', version: 2 },
qmcogg: { ext: 'ogg', version: 2 }, qmcogg: { ext: 'ogg', version: 2 },
qmc0: { ext: 'mp3', version: 1 }, qmc0: { ext: 'mp3', version: 2 },
qmc2: { ext: 'ogg', version: 1 }, qmc2: { ext: 'ogg', version: 2 },
qmc3: { ext: 'mp3', version: 1 }, qmc3: { ext: 'mp3', version: 2 },
bkcmp3: { ext: 'mp3', version: 1 }, bkcmp3: { ext: 'mp3', version: 1 },
bkcflac: { ext: 'flac', version: 1 }, bkcflac: { ext: 'flac', version: 1 },
tkm: { ext: 'm4a', version: 1 }, tkm: { ext: 'm4a', version: 1 },
@ -49,13 +49,14 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
if (version === 2 && globalThis.WebAssembly) { if (version === 2 && globalThis.WebAssembly) {
console.log('qmc: using wasm decoder'); console.log('qmc: using wasm decoder');
const v2Decrypted = await DecryptQMCWasm(fileBuffer); const v2Decrypted = await DecryptQmcWasm(fileBuffer, raw_ext);
// 若 v2 检测失败,降级到 v1 再尝试一次 // 若 v2 检测失败,降级到 v1 再尝试一次
if (v2Decrypted.success) { if (v2Decrypted.success) {
musicDecoded = v2Decrypted.data; musicDecoded = v2Decrypted.data;
musicID = v2Decrypted.songId; musicID = v2Decrypted.songId;
console.log('qmc wasm decoder suceeded');
} else { } else {
console.warn('qmc2-wasm failed with error %s', v2Decrypted.error || '(no error)'); console.warn('QmcWasm failed with error %s', v2Decrypted.error || '(unknown error)');
} }
} }
@ -151,7 +152,7 @@ export class QmcDecoder {
} else { } else {
const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset); const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset);
const keySize = sizeView.getUint32(0, true); const keySize = sizeView.getUint32(0, true);
if (keySize < 0x300) { if (keySize < 0x400) {
this.audioSize = this.size - keySize - 4; this.audioSize = this.size - keySize - 4;
const rawKey = this.file.subarray(this.audioSize, this.size - 4); const rawKey = this.file.subarray(this.audioSize, this.size - 4);
this.setCipher(rawKey); this.setCipher(rawKey);

26
src/decrypt/qmc_key.ts

@ -5,12 +5,14 @@ const ZERO_LEN = 7;
export function QmcDeriveKey(raw: Uint8Array): Uint8Array { export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
const textDec = new TextDecoder(); const textDec = new TextDecoder();
const rawDec = Buffer.from(textDec.decode(raw), 'base64'); let rawDec = Buffer.from(textDec.decode(raw), 'base64');
let n = rawDec.length; let n = rawDec.length;
if (n < 16) { if (n < 16) {
throw Error('key length is too short'); throw Error('key length is too short');
} }
rawDec = decryptV2Key(rawDec);
const simpleKey = simpleMakeKey(106, 8); const simpleKey = simpleMakeKey(106, 8);
let teaKey = new Uint8Array(16); let teaKey = new Uint8Array(16);
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
@ -32,6 +34,28 @@ export function simpleMakeKey(salt: number, length: number): number[] {
return keyBuf; 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 { function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
if (inBuf.length % 8 != 0) { if (inBuf.length % 8 != 0) {
throw Error('inBuf size not a multiple of the block size'); throw Error('inBuf size not a multiple of the block size');

100
src/decrypt/qmc_wasm.ts

@ -1,14 +1,10 @@
import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle'; import QmcCryptoModule from '@/QmcWasm/QmcWasmBundle';
import { MergeUint8Array } from '@/utils/MergeUint8Array'; import { MergeUint8Array } from '@/utils/MergeUint8Array';
import { QMCCrypto } from '@jixun/qmc2-crypto/QMCCrypto';
// 检测文件末端使用的缓冲区大小
const DETECTION_SIZE = 40;
// 每次处理 2M 的数据 // 每次处理 2M 的数据
const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024; const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
export interface QMC2DecryptionResult { export interface QMCDecryptionResult {
success: boolean; success: boolean;
data: Uint8Array; data: Uint8Array;
songId: string | number; songId: string | number;
@ -16,96 +12,62 @@ export interface QMC2DecryptionResult {
} }
/** /**
* QMC2 * QMC
* *
* Uint8Array * Uint8Array
* @param {ArrayBuffer} mggBlob Blob * @param {ArrayBuffer} qmcBlob Blob
*/ */
export async function DecryptQMCWasm(mggBlob: ArrayBuffer): Promise<QMC2DecryptionResult> { export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise<QMCDecryptionResult> {
const result: QMC2DecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' }; const result: QMCDecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
// 初始化模组 // 初始化模组
let QMCCrypto: QMCCrypto; let QmcCrypto: any;
try { try {
QMCCrypto = await QMCCryptoModule(); QmcCrypto = await QmcCryptoModule();
} catch (err: any) { } catch (err: any) {
result.error = err?.message || 'wasm 加载失败'; result.error = err?.message || 'wasm 加载失败';
return result; return result;
} }
if (!QmcCrypto) {
// 申请内存块,并文件末端数据到 WASM 的内存堆 result.error = 'wasm 加载失败';
const detectionBuf = new Uint8Array(mggBlob.slice(-DETECTION_SIZE)); return result;
const pDetectionBuf = QMCCrypto._malloc(detectionBuf.length);
QMCCrypto.writeArrayToMemory(detectionBuf, pDetectionBuf);
// 检测结果内存块
const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection());
// 进行检测
const detectOK = QMCCrypto.detectKeyEndPosition(pDetectionResult, pDetectionBuf, detectionBuf.length);
// 提取结构体内容:
// (pos: i32; len: i32; error: char[??])
const position = QMCCrypto.getValue(pDetectionResult, 'i32');
const len = QMCCrypto.getValue(pDetectionResult + 4, 'i32');
result.success = detectOK;
result.error = QMCCrypto.UTF8ToString(
pDetectionResult + QMCCrypto.offsetof_error_msg(),
QMCCrypto.sizeof_error_msg(),
);
const songId = QMCCrypto.UTF8ToString(pDetectionResult + QMCCrypto.offsetof_song_id(), QMCCrypto.sizeof_song_id());
if (!songId) {
console.debug('qmc2-wasm: songId not found');
} else if (/^\d+$/.test(songId)) {
result.songId = songId;
} else {
console.warn('qmc2-wasm: Invalid songId: %s', songId);
} }
// 释放内存 // 申请内存块,并文件末端数据到 WASM 的内存堆
QMCCrypto._free(pDetectionBuf); const qmcBuf = new Uint8Array(qmcBlob);
QMCCrypto._free(pDetectionResult); const pQmcBuf = QmcCrypto._malloc(DECRYPTION_BUF_SIZE);
QmcCrypto.writeArrayToMemory(qmcBuf.slice(-DECRYPTION_BUF_SIZE), pQmcBuf);
if (!detectOK) {
// 进行解密初始化
ext = '.' + ext;
const tailSize = QmcCrypto.preDec(pQmcBuf, DECRYPTION_BUF_SIZE, ext);
if (tailSize == -1) {
result.error = QmcCrypto.getError();
return result; return result;
} else {
result.songId = QmcCrypto.getSongId();
result.songId = result.songId == "0" ? 0 : result.songId;
} }
// 计算解密后文件的大小。
// 之前得到的 position 为相对当前检测数据起点的偏移。
const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position;
// 提取嵌入到文件的 EKey
const ekey = new Uint8Array(mggBlob.slice(decryptedSize, decryptedSize + len));
// 解码 UTF-8 数据到 string
const decoder = new TextDecoder();
const ekey_b64 = decoder.decode(ekey);
// 初始化加密与缓冲区
const hCrypto = QMCCrypto.createInstWidthEKey(ekey_b64);
const buf = QMCCrypto._malloc(DECRYPTION_BUF_SIZE);
const decryptedParts = []; const decryptedParts = [];
let offset = 0; let offset = 0;
let bytesToDecrypt = decryptedSize; let bytesToDecrypt = qmcBuf.length - tailSize;
while (bytesToDecrypt > 0) { while (bytesToDecrypt > 0) {
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE); const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
// 解密一些片段 // 解密一些片段
const blockData = new Uint8Array(mggBlob.slice(offset, offset + blockSize)); const blockData = new Uint8Array(qmcBuf.slice(offset, offset + blockSize));
QMCCrypto.writeArrayToMemory(blockData, buf); QmcCrypto.writeArrayToMemory(blockData, pQmcBuf);
QMCCrypto.decryptStream(hCrypto, buf, offset, blockSize); decryptedParts.push(QmcCrypto.HEAPU8.slice(pQmcBuf, pQmcBuf + QmcCrypto.decBlob(pQmcBuf, blockSize, offset)));
decryptedParts.push(QMCCrypto.HEAPU8.slice(buf, buf + blockSize));
offset += blockSize; offset += blockSize;
bytesToDecrypt -= blockSize; bytesToDecrypt -= blockSize;
} }
QMCCrypto._free(buf); QmcCrypto._free(pQmcBuf);
hCrypto.delete();
result.data = MergeUint8Array(decryptedParts); result.data = MergeUint8Array(decryptedParts);
result.success = true;
return result; return result;
} }

41
src/decrypt/qmccache.ts

@ -8,36 +8,51 @@ import {
} from '@/decrypt/utils'; } from '@/decrypt/utils';
import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc'; import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc';
import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
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 function DecryptBuffer(buffer: Uint8Array | Buffer) { export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
let length = buffer.byteLength; 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++) { for (let i = 0; i < length; i++) {
let byte = buffer[i] ^ 0xf4; // xor 0xf4 let byte = musicDecoded[i] ^ 0xf4; // xor 0xf4
byte = ((byte & 0b0011_1111) << 2) | (byte >> 6); // rol 2 byte = ((byte & 0b0011_1111) << 2) | (byte >> 6); // rol 2
buffer[i] = byte; musicDecoded[i] = byte;
} }
} }
let ext = SniffAudioExt(musicDecoded, '');
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
const buffer = new Uint8Array(await GetArrayBuffer(file));
DecryptBuffer(buffer);
let ext = SniffAudioExt(buffer, '');
const newName = SplitFilename(raw_filename); const newName = SplitFilename(raw_filename);
let audioBlob: Blob; let audioBlob: Blob;
if (ext !== '' || newName.ext === 'mp3') { if (ext !== '' || newName.ext === 'mp3') {
audioBlob = new Blob([buffer], { type: AudioMimeType[ext] }); audioBlob = new Blob([musicDecoded], { type: AudioMimeType[ext] });
} else if (newName.ext in HandlerMap) { } else if (newName.ext in HandlerMap) {
audioBlob = new Blob([buffer], { type: 'application/octet-stream' }); audioBlob = new Blob([musicDecoded], { type: 'application/octet-stream' });
return QmcDecrypt(audioBlob, newName.name, newName.ext); return QmcDecrypt(audioBlob, newName.name, newName.ext);
} else { } else {
throw '不支持的QQ音乐缓存格式'; throw '不支持的QQ音乐缓存格式';
} }
const tag = await metaParseBlob(audioBlob); const tag = await metaParseBlob(audioBlob);
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist); const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ""));
return { return {
title, title,

2
src/decrypt/raw.ts

@ -17,7 +17,7 @@ export async function Decrypt(
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] }); if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
} }
const tag = await metaParseBlob(file); const tag = await metaParseBlob(file);
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist); const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ''));
return { return {
title, title,

88
src/decrypt/utils.ts

@ -2,6 +2,8 @@ import { IAudioMetadata } from 'music-metadata-browser';
import ID3Writer from 'browser-id3-writer'; import ID3Writer from 'browser-id3-writer';
import MetaFlac from 'metaflac-js'; import MetaFlac from 'metaflac-js';
export const split_regex = /[ ]?[,;/_、][ ]?/;
export const FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43]; export const FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43];
export const MP3_HEADER = [0x49, 0x44, 0x33]; export const MP3_HEADER = [0x49, 0x44, 0x33];
export const OGG_HEADER = [0x4f, 0x67, 0x67, 0x53]; export const OGG_HEADER = [0x4f, 0x67, 0x67, 0x53];
@ -91,7 +93,8 @@ export function GetMetaFromFile(
const items = filename.split(separator); const items = filename.split(separator);
if (items.length > 1) { if (items.length > 1) {
if (!meta.artist) meta.artist = items[0].trim(); //由文件名和原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(); if (!meta.title) meta.title = items[1].trim();
} else if (items.length === 1) { } else if (items.length === 1) {
if (!meta.title) meta.title = items[0].trim(); if (!meta.title) meta.title = items[0].trim();
@ -119,6 +122,8 @@ export interface IMusicMeta {
title: string; title: string;
artists?: string[]; artists?: string[];
album?: string; album?: string;
albumartist?: string;
genre?: string[];
picture?: ArrayBuffer; picture?: ArrayBuffer;
picture_desc?: string; picture_desc?: string;
} }
@ -132,7 +137,9 @@ export function WriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IA
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') { if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
try { try {
writer.setFrame(frame.id, frame.value); writer.setFrame(frame.id, frame.value);
} catch (e) {} } catch (e) {
console.warn(`failed to write ID3 tag '${frame.id}'`);
}
} }
}); });
@ -169,6 +176,83 @@ export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: I
return writer.save(); 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 } { export function SplitFilename(n: string): { name: string; ext: string } {
const pos = n.lastIndexOf('.'); const pos = n.lastIndexOf('.');
return { return {

2
src/decrypt/xm.ts

@ -49,7 +49,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
const { title, artist } = GetMetaFromFile( const { title, artist } = GetMetaFromFile(
raw_filename, raw_filename,
musicMeta.common.title, musicMeta.common.title,
musicMeta.common.artist, String(musicMeta.common.artists || musicMeta.common.artist || ""),
raw_filename.indexOf('_') === -1 ? '-' : '_', raw_filename.indexOf('_') === -1 ? '-' : '_',
); );

23
src/utils/qm_meta.ts

@ -8,6 +8,7 @@ import {
WriteMetaToFlac, WriteMetaToFlac,
WriteMetaToMp3, WriteMetaToMp3,
AudioMimeType, AudioMimeType,
split_regex,
} from '@/decrypt/utils'; } from '@/decrypt/utils';
import { getQMImageURLFromPMID, queryAlbumCover, querySongInfoById } from '@/utils/api'; import { getQMImageURLFromPMID, queryAlbumCover, querySongInfoById } from '@/utils/api';
@ -19,6 +20,8 @@ interface MetaResult {
blob: Blob; blob: Blob;
} }
const fromGBK = (text?: string) => iconv.decode(new Buffer(text || ''), 'gbk');
/** /**
* *
* @param musicBlob * @param musicBlob
@ -38,13 +41,19 @@ export async function extractQQMusicMeta(
if (!musicMeta.native.hasOwnProperty(metaIdx)) continue; if (!musicMeta.native.hasOwnProperty(metaIdx)) continue;
if (musicMeta.native[metaIdx].some((item) => item.id === 'TCON' && item.value === '(12)')) { if (musicMeta.native[metaIdx].some((item) => item.id === 'TCON' && item.value === '(12)')) {
console.warn('try using gbk encoding to decode meta'); console.warn('try using gbk encoding to decode meta');
musicMeta.common.artist = iconv.decode(new Buffer(musicMeta.common.artist ?? ''), 'gbk'); musicMeta.common.artist = '';
musicMeta.common.title = iconv.decode(new Buffer(musicMeta.common.title ?? ''), 'gbk'); if (!musicMeta.common.artists) {
musicMeta.common.album = iconv.decode(new Buffer(musicMeta.common.album ?? ''), 'gbk'); musicMeta.common.artist = fromGBK(musicMeta.common.artist);
}
else {
musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join();
}
musicMeta.common.title = fromGBK(musicMeta.common.title);
musicMeta.common.album = fromGBK(musicMeta.common.album);
} }
} }
if (id) { if (id && id !== '0') {
try { try {
return fetchMetadataFromSongId(id, ext, musicMeta, musicBlob); return fetchMetadataFromSongId(id, ext, musicMeta, musicBlob);
} catch (e) { } catch (e) {
@ -62,12 +71,12 @@ export async function extractQQMusicMeta(
return { return {
title: info.title, title: info.title,
artist: info.artist || '', artist: info.artist,
album: musicMeta.common.album || '', album: musicMeta.common.album || '',
imgUrl: imageURL, imgUrl: imageURL,
blob: await writeMetaToAudioFile({ blob: await writeMetaToAudioFile({
title: info.title, title: info.title,
artists: info.artist.split(' _ '), artists: info.artist.split(split_regex),
ext, ext,
imageURL, imageURL,
musicMeta, musicMeta,
@ -88,7 +97,7 @@ async function fetchMetadataFromSongId(
return { return {
title: info.track_info.title, title: info.track_info.title,
artist: artists.join(''), artist: artists.join(','),
album: info.track_info.album.name, album: info.track_info.album.name,
imgUrl: imageURL, imgUrl: imageURL,

87
src/view/Home.vue

@ -10,6 +10,15 @@
</el-radio> </el-radio>
</el-row> </el-row>
<el-row> <el-row>
<edit-dialog
:show="showEditDialog"
:picture="editing_data.picture"
:title="editing_data.title"
:artist="editing_data.artist"
:album="editing_data.album"
:albumartist="editing_data.albumartist"
:genre="editing_data.genre"
@cancel="showEditDialog = false" @ok="handleEdit"></edit-dialog>
<config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></config-dialog> <config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></config-dialog>
<el-tooltip class="item" effect="dark" placement="top"> <el-tooltip class="item" effect="dark" placement="top">
<div slot="content"> <div slot="content">
@ -35,7 +44,7 @@
<audio :autoplay="playing_auto" :src="playing_url" controls /> <audio :autoplay="playing_auto" :src="playing_url" controls />
<PreviewTable :policy="filename_policy" :table-data="tableData" @download="saveFile" @play="changePlaying" /> <PreviewTable :policy="filename_policy" :table-data="tableData" @download="saveFile" @edit="editFile" @play="changePlaying" />
</div> </div>
</template> </template>
@ -43,8 +52,11 @@
import FileSelector from '@/component/FileSelector'; import FileSelector from '@/component/FileSelector';
import PreviewTable from '@/component/PreviewTable'; import PreviewTable from '@/component/PreviewTable';
import ConfigDialog from '@/component/ConfigDialog'; import ConfigDialog from '@/component/ConfigDialog';
import EditDialog from '@/component/EditDialog';
import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils'; import { DownloadBlobMusic, FilenamePolicy, FilenamePolicies, RemoveBlobMusic, DirectlyWriteFile } from '@/utils/utils';
import { GetImageFromURL, RewriteMetaToMp3, RewriteMetaToFlac, AudioMimeType, split_regex } from '@/decrypt/utils';
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
export default { export default {
name: 'Home', name: 'Home',
@ -52,10 +64,13 @@ export default {
FileSelector, FileSelector,
PreviewTable, PreviewTable,
ConfigDialog, ConfigDialog,
EditDialog,
}, },
data() { data() {
return { return {
showConfigDialog: false, showConfigDialog: false,
showEditDialog: false,
editing_data: { picture: '', title: '', artist: '', album: '', albumartist: '', genre: '', },
tableData: [], tableData: [],
playing_url: '', playing_url: '',
playing_auto: false, playing_auto: false,
@ -128,7 +143,77 @@ export default {
} }
}, 300); }, 300);
}, },
async handleEdit(data) {
this.showEditDialog = false;
URL.revokeObjectURL(this.editing_data.file);
if (data.picture) {
URL.revokeObjectURL(this.editing_data.picture);
this.editing_data.picture = URL.createObjectURL(data.picture);
}
this.editing_data.title = data.title;
this.editing_data.artist = data.artist;
this.editing_data.album = data.album;
let writeSuccess = true;
let notifyMsg = '成功修改 ' + this.editing_data.title;
try {
const musicMeta = await metaParseBlob(new Blob([this.editing_data.blob], { type: mime }));
let imageInfo = undefined;
if (this.editing_data.picture !== '') {
imageInfo = await GetImageFromURL(this.editing_data.picture);
if (!imageInfo) {
console.warn('获取图像失败', this.editing_data.picture);
}
}
const newMeta = { picture: imageInfo?.buffer,
title: data.title,
artists: data.artist.split(split_regex),
album: data.album,
albumartist: data.albumartist,
genre: data.genre.split(split_regex)
};
const buffer = Buffer.from(await this.editing_data.blob.arrayBuffer());
const mime = AudioMimeType[this.editing_data.ext] || AudioMimeType.mp3;
if (this.editing_data.ext === 'mp3') {
this.editing_data.blob = new Blob([RewriteMetaToMp3(buffer, newMeta, musicMeta)], { type: mime });
} else if (this.editing_data.ext === 'flac') {
this.editing_data.blob = new Blob([RewriteMetaToFlac(buffer, newMeta, musicMeta)], { type: mime });
} else {
writeSuccess = undefined;
notifyMsg = this.editing_data.ext + '类型文件暂时不支持修改音乐标签';
}
} catch (e) {
writeSuccess = false;
notifyMsg = '修改' + this.editing_data.title + '未能完成。在写入新的元数据时发生错误:' + e;
}
this.editing_data.file = URL.createObjectURL(this.editing_data.blob);
if (writeSuccess === true) {
this.$notify.success({
title: '修改成功',
message: notifyMsg,
duration: 3000,
});
} else if (writeSuccess === false) {
this.$notify.error({
title: '修改失败',
message: notifyMsg,
duration: 3000,
});
} else {
this.$notify.warning({
title: '修改取消',
message: notifyMsg,
duration: 3000,
});
}
},
async editFile(data) {
this.editing_data = data;
const musicMeta = await metaParseBlob(this.editing_data.blob);
this.editing_data.albumartist = musicMeta.common.albumartist || '';
this.editing_data.genre = musicMeta.common.genre?.toString() || '';
this.showEditDialog = true;
},
async saveFile(data) { async saveFile(data) {
if (this.dir) { if (this.dir) {
await DirectlyWriteFile(data, this.filename_policy, this.dir); await DirectlyWriteFile(data, this.filename_policy, this.dir);

Loading…
Cancel
Save