Merge pull request 'Add Tag Edit Function & Wasm for Qmc & Kgm' (#5) from xhacker-zzz/web:master into master
Reviewed-on: um/web#5
This commit is contained in:
commit
b44d38989b
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -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
vendored
3
.github/workflows/release-build.yml
vendored
@ -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
vendored
7
.gitignore
vendored
@ -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
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
Executable file
11
build-wasm
Executable file
@ -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
generated
31
package-lock.json
generated
@ -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",
|
||||||
|
@ -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
Normal file
65
src/KgmWasm/CMakeLists.txt
Normal 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
20
src/KgmWasm/KgmWasm.cpp
Normal 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
18
src/KgmWasm/KgmWasm.h
Normal 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
9
src/KgmWasm/README.md
Normal 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
41
src/KgmWasm/build-wasm
Executable 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
|
||||||
|
|
||||||
|
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
Normal file
112
src/KgmWasm/kgm.hpp
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
src/QmcWasm/CMakeLists.txt
Normal file
65
src/QmcWasm/CMakeLists.txt
Normal 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
57
src/QmcWasm/QmcWasm.cpp
Normal 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
23
src/QmcWasm/QmcWasm.h
Normal 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
9
src/QmcWasm/README.md
Normal 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
289
src/QmcWasm/TencentTea.hpp
Normal 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
207
src/QmcWasm/base64.hpp
Normal 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
41
src/QmcWasm/build-wasm
Executable 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
|
||||||
|
|
||||||
|
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
Normal file
230
src/QmcWasm/qmc.hpp
Normal 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 == "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
Normal file
290
src/QmcWasm/qmc_cipher.hpp
Normal 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
217
src/QmcWasm/qmc_key.hpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
178
src/component/EditDialog.vue
Normal file
178
src/component/EditDialog.vue
Normal 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>
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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 bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer);
|
let musicDecoded: Uint8Array | undefined;
|
||||||
let headerLen = bHeaderLen.getUint32(0, true);
|
if (globalThis.WebAssembly) {
|
||||||
|
console.log('kgm: using wasm decoder');
|
||||||
|
|
||||||
let audioData = oriData.slice(headerLen);
|
const kgmDecrypted = await DecryptKgmWasm(oriData, raw_ext);
|
||||||
let dataLen = audioData.length;
|
if (kgmDecrypted.success) {
|
||||||
|
musicDecoded = kgmDecrypted.data;
|
||||||
let key1 = Array.from(oriData.slice(0x1c, 0x2c));
|
console.log('kgm wasm decoder suceeded');
|
||||||
key1.push(0);
|
} else {
|
||||||
|
console.warn('KgmWasm failed with error %s', kgmDecrypted.error || '(unknown error)');
|
||||||
const decryptByte = raw_ext === 'vpr' ? decryptVprByteAtOffset : decryptKgmByteAtOffsetV2;
|
}
|
||||||
for (let i = 0; i < dataLen; i++) {
|
|
||||||
audioData[i] = decryptByte(audioData[i], key1, i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = SniffAudioExt(audioData);
|
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];
|
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
Normal file
67
src/decrypt/kgm_wasm.ts
Normal 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;
|
||||||
|
}
|
@ -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
Normal file
71
src/decrypt/mg3d.ts
Normal 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);
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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');
|
||||||
|
@ -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));
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放内存
|
|
||||||
QMCCrypto._free(pDetectionBuf);
|
|
||||||
QMCCrypto._free(pDetectionResult);
|
|
||||||
|
|
||||||
if (!detectOK) {
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算解密后文件的大小。
|
// 申请内存块,并文件末端数据到 WASM 的内存堆
|
||||||
// 之前得到的 position 为相对当前检测数据起点的偏移。
|
const qmcBuf = new Uint8Array(qmcBlob);
|
||||||
const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position;
|
const pQmcBuf = QmcCrypto._malloc(DECRYPTION_BUF_SIZE);
|
||||||
|
QmcCrypto.writeArrayToMemory(qmcBuf.slice(-DECRYPTION_BUF_SIZE), pQmcBuf);
|
||||||
|
|
||||||
// 提取嵌入到文件的 EKey
|
// 进行解密初始化
|
||||||
const ekey = new Uint8Array(mggBlob.slice(decryptedSize, decryptedSize + len));
|
ext = '.' + ext;
|
||||||
|
const tailSize = QmcCrypto.preDec(pQmcBuf, DECRYPTION_BUF_SIZE, ext);
|
||||||
// 解码 UTF-8 数据到 string
|
if (tailSize == -1) {
|
||||||
const decoder = new TextDecoder();
|
result.error = QmcCrypto.getError();
|
||||||
const ekey_b64 = decoder.decode(ekey);
|
return result;
|
||||||
|
} else {
|
||||||
// 初始化加密与缓冲区
|
result.songId = QmcCrypto.getSongId();
|
||||||
const hCrypto = QMCCrypto.createInstWidthEKey(ekey_b64);
|
result.songId = result.songId == "0" ? 0 : result.songId;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -1,52 +1,67 @@
|
|||||||
import {
|
import {
|
||||||
AudioMimeType,
|
AudioMimeType,
|
||||||
GetArrayBuffer,
|
GetArrayBuffer,
|
||||||
GetCoverFromFile,
|
GetCoverFromFile,
|
||||||
GetMetaFromFile,
|
GetMetaFromFile,
|
||||||
SniffAudioExt,
|
SniffAudioExt,
|
||||||
SplitFilename,
|
SplitFilename,
|
||||||
} 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) {
|
|
||||||
let length = buffer.byteLength;
|
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||||
for (let i = 0; i < length; i++) {
|
const buffer = await GetArrayBuffer(file);
|
||||||
let byte = buffer[i] ^ 0xf4; // xor 0xf4
|
|
||||||
byte = ((byte & 0b0011_1111) << 2) | (byte >> 6); // rol 2
|
let musicDecoded: Uint8Array | undefined;
|
||||||
buffer[i] = byte;
|
if (globalThis.WebAssembly) {
|
||||||
}
|
console.log('qmc: using wasm decoder');
|
||||||
}
|
|
||||||
|
const qmcDecrypted = await DecryptQmcWasm(buffer, raw_ext);
|
||||||
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
|
// 若 wasm 失败,使用 js 再尝试一次
|
||||||
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
if (qmcDecrypted.success) {
|
||||||
DecryptBuffer(buffer);
|
musicDecoded = qmcDecrypted.data;
|
||||||
let ext = SniffAudioExt(buffer, '');
|
console.log('qmc wasm decoder suceeded');
|
||||||
const newName = SplitFilename(raw_filename);
|
} else {
|
||||||
let audioBlob: Blob;
|
console.warn('QmcWasm failed with error %s', qmcDecrypted.error || '(unknown error)');
|
||||||
if (ext !== '' || newName.ext === 'mp3') {
|
}
|
||||||
audioBlob = new Blob([buffer], { type: AudioMimeType[ext] });
|
}
|
||||||
} else if (newName.ext in HandlerMap) {
|
|
||||||
audioBlob = new Blob([buffer], { type: 'application/octet-stream' });
|
if (!musicDecoded) {
|
||||||
return QmcDecrypt(audioBlob, newName.name, newName.ext);
|
musicDecoded = new Uint8Array(buffer);
|
||||||
} else {
|
let length = musicDecoded.length;
|
||||||
throw '不支持的QQ音乐缓存格式';
|
for (let i = 0; i < length; i++) {
|
||||||
}
|
let byte = musicDecoded[i] ^ 0xf4; // xor 0xf4
|
||||||
const tag = await metaParseBlob(audioBlob);
|
byte = ((byte & 0b0011_1111) << 2) | (byte >> 6); // rol 2
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
|
musicDecoded[i] = byte;
|
||||||
|
}
|
||||||
return {
|
}
|
||||||
title,
|
let ext = SniffAudioExt(musicDecoded, '');
|
||||||
artist,
|
const newName = SplitFilename(raw_filename);
|
||||||
ext,
|
let audioBlob: Blob;
|
||||||
album: tag.common.album,
|
if (ext !== '' || newName.ext === 'mp3') {
|
||||||
picture: GetCoverFromFile(tag),
|
audioBlob = new Blob([musicDecoded], { type: AudioMimeType[ext] });
|
||||||
file: URL.createObjectURL(audioBlob),
|
} else if (newName.ext in HandlerMap) {
|
||||||
blob: audioBlob,
|
audioBlob = new Blob([musicDecoded], { type: 'application/octet-stream' });
|
||||||
mime: AudioMimeType[ext],
|
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],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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 ? '-' : '_',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
|
||||||
|
@ -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…
Reference in New Issue
Block a user