From 34522bfe905b85bda7355907794ffd97e38457fc Mon Sep 17 00:00:00 2001 From: xhacker-zzz <63585800+xhacker-zzz@users.noreply.github.com> Date: Thu, 19 Jan 2023 23:53:57 +0800 Subject: [PATCH] add source --- CMakeLists.txt | 65 +++++++++++ QmcWasm.cpp | 57 ++++++++++ QmcWasm.h | 23 ++++ TencentTea.hpp | 289 ++++++++++++++++++++++++++++++++++++++++++++++++ base64.hpp | 207 +++++++++++++++++++++++++++++++++++ qmc.hpp | 230 +++++++++++++++++++++++++++++++++++++++ qmc_cipher.hpp | 290 +++++++++++++++++++++++++++++++++++++++++++++++++ qmc_key.hpp | 217 ++++++++++++++++++++++++++++++++++++ qmc_wasm.ts | 74 +++++++++++++ 9 files changed, 1452 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 QmcWasm.cpp create mode 100644 QmcWasm.h create mode 100644 TencentTea.hpp create mode 100644 base64.hpp create mode 100644 qmc.hpp create mode 100644 qmc_cipher.hpp create mode 100644 qmc_key.hpp create mode 100644 qmc_wasm.ts diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..066268a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,65 @@ +# CMakeList.txt : CMake project for QmcWasm, include source and define +# project specific logic here. +# +cmake_minimum_required (VERSION 3.8) + +project ("QmcWasm") + +set(CMAKE_CXX_STANDARD 14) + +include_directories( + $ +) + +# 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} +) + diff --git a/QmcWasm.cpp b/QmcWasm.cpp new file mode 100644 index 0000000..f4fc8c0 --- /dev/null +++ b/QmcWasm.cpp @@ -0,0 +1,57 @@ +// QmcWasm.cpp : Defines the entry point for the application. +// + +#include "QmcWasm.h" + +#include "qmc.hpp" + +#include +#include + +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 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; +} diff --git a/QmcWasm.h b/QmcWasm.h new file mode 100644 index 0000000..6fd63bf --- /dev/null +++ b/QmcWasm.h @@ -0,0 +1,23 @@ +// QmcWasm.h : Include file for standard system include files, +// or project specific include files. + +#pragma once + +#include +#include + +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()); +} diff --git a/TencentTea.hpp b/TencentTea.hpp new file mode 100644 index 0000000..4f635a7 --- /dev/null +++ b/TencentTea.hpp @@ -0,0 +1,289 @@ +#ifndef QQMUSIC_CPP_TENCENTTEA_HPP +#define QQMUSIC_CPP_TENCENTTEA_HPP + +#include +#include +#include +#include +#include +#include + +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 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 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ΪܵIJ(Body); + :Ϊܺij(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+Zeroij*/; + 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*/ + { + /*ģ80貹0,17,26,...,71*/ + nPadlen = 8 - nPadlen; + } + + return nPadlen; +} + +/*pKeyΪ16byte*/ +/* + :pInBufΪܵIJ(Body),nInBufLenΪpInBuf; + :pOutBufΪĸʽ,pOutBufLenΪpOutBufij8byteı; +*/ +/*TEA㷨,CBCģʽ*/ +/*ĸʽ:PadLen(1byte)+Padding(var,0-7byte)+Salt(2byte)+Body(var byte)+Zero(7byte)*/ +bool encryptTencentTea(std::vector inBuf, std::vector key, std::vector &outBuf) +{ + srand(time(0)); + int nPadlen = encryptTencentTeaLen(inBuf.size()); + size_t ivCrypt; + std::vector srcBuf; + srcBuf.resize(8); + std::vector 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++) /*ǰǰ8byte(iv_cryptָ)*/\ + srcBuf[j] ^= outBuf[j + ivCrypt];\ + /*pOutBufferpInBufferΪ8byte, pKeyΪ16byte*/\ + /**/\ + TeaEncryptECB(srcBuf.data(), outBuf.data()+outBufPos, key, 16);\ + for (j = 0; j < 8; j++) /*ܺǰ8byte(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 inBuf, std::vector key, std::vector &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 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 outBuf; + outBuf.resize(outLen); + + std::vector ivPrev; + ivPrev.resize(8); + std::vector 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 diff --git a/base64.hpp b/base64.hpp new file mode 100644 index 0000000..b3b6aca --- /dev/null +++ b/base64.hpp @@ -0,0 +1,207 @@ +// +// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// + +/* + Portions from http://www.adp-gmbh.ch/cpp/common/base64.html + Copyright notice: + + base64.cpp and base64.h + + Copyright (C) 2004-2008 Rene Nyffenegger + + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + + 3. This notice may not be removed or altered from any source distribution. + + Rene Nyffenegger rene.nyffenegger@adp-gmbh.ch +*/ + +#ifndef BASE64_HPP +#define BASE64_HPP + +#include +#include +#include + +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(dest); + char const* in = static_cast(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(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 + decode(void* dest, char const* src, std::size_t len) + { + char* out = static_cast(dest); + auto in = reinterpret_cast(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(dest), + in - reinterpret_cast(src) }; + } + +} // base64 + +#endif diff --git a/qmc.hpp b/qmc.hpp new file mode 100644 index 0000000..3f26ecf --- /dev/null +++ b/qmc.hpp @@ -0,0 +1,230 @@ +#include +#include +#include +#include +#include "qmc_key.hpp" +#include "qmc_cipher.hpp" + +class QmcDecode { +private: + std::vector blobData; + + std::vector rawKeyBuf; + std::string cipherType = ""; + + size_t dataOffset = 0; + size_t keySize = 0; + int mediaVer = 0; + + std::string checkType(std::string fn) { + if (fn.find(".qmc") < fn.size() || fn.find(".m") < fn.size()) + { + std::string buf_tag = ""; + for (int i = 4; i > 0; --i) + { + buf_tag += *((char*)blobData.data() + blobData.size() - i); + } + if (buf_tag == "QTag") + { + keySize = ntohl(*(uint32_t*)(blobData.data() + blobData.size() - 8)); + return "QTag"; + } + else if (buf_tag == "STag") + { + return "STag"; + } + else + { + keySize = (*(uint32_t*)(blobData.data() + blobData.size() - 4)); + if (keySize < 0x400) + { + return "Map/RC4"; + } + else + { + keySize = 0; + return "Static"; + } + } + } + else if (fn.find(".cache") < fn.size()) + { + return "cache"; + } + else if (fn.find(".tm") < fn.size()) + { + return "ios"; + } + else + { + return "invalid"; + } + } + + bool parseRawKeyQTag() { + std::string ketStr = ""; + std::string::size_type index = 0; + ketStr.append((char*)rawKeyBuf.data(), rawKeyBuf.size()); + index = ketStr.find(",", 0); + if (index != std::string::npos) + { + rawKeyBuf.resize(index); + } + else + { + return false; + } + ketStr = ketStr.substr(index + 1); + index = ketStr.find(",", 0); + if (index != std::string::npos) + { + this->songId = ketStr.substr(0, index); + } + else + { + return false; + } + ketStr = ketStr.substr(index + 1); + index = ketStr.find(",", 0); + if (index == std::string::npos) + { + this->mediaVer = std::stoi(ketStr); + } + else + { + return false; + } + return true; + } + + bool readRawKey(size_t tailSize) { + // get raw key data length + rawKeyBuf.resize(keySize); + if (rawKeyBuf.size() != keySize) { + return false; + } + for (size_t i = 0; i < keySize; i++) + { + rawKeyBuf[i] = blobData[i + blobData.size() - (tailSize + keySize)]; + } + return true; + } + + void DecodeStatic(); + + void DecodeMapRC4(); + + void DecodeCache(); + + void DecodeTm(); + +public: + bool SetBlob(uint8_t* blob, size_t blobSize) { + blobData.resize(blobSize); + if (blobData.size() != blobSize) { + return false; + } + memcpy(blobData.data(), blob, blobSize); + return true; + } + + int PreDecode(std::string ext) { + cipherType = checkType(ext); + size_t tailSize = 0; + if (cipherType == "invalid" || cipherType == "STag") { + error = "file is invalid or not supported (Please downgrade your app)."; + return -1; + } + if (cipherType == "QTag") { + tailSize = 8; + } + else if (cipherType == "Map/RC4") { + tailSize = 4; + } + if (keySize > 0) { + if (!readRawKey(tailSize)) { + error = "cannot read embedded key from file"; + return -1; + } + if (tailSize == 8) { + cipherType = "Map/RC4"; + if (!parseRawKeyQTag()) { + error = "cannot parse embedded key"; + return -1; + } + } + std::vector tmp; + if (!QmcDecryptKey(rawKeyBuf, tmp)) { + error = "cannot decrypt embedded key"; + return -1; + } + rawKeyBuf = tmp; + } + return keySize + tailSize; + } + + std::vector 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 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; +} diff --git a/qmc_cipher.hpp b/qmc_cipher.hpp new file mode 100644 index 0000000..8dc2b18 --- /dev/null +++ b/qmc_cipher.hpp @@ -0,0 +1,290 @@ +#include +#include +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& buf, size_t offset) { + for (size_t i = 0; i < buf.size(); i++) { + buf[i] ^= getMask(offset + i); + } + } +}; + +class QmcMapCipher { +private: + std::vector 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 &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& 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& 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 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& 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 S; + std::vector key; + uint32_t hash = 1; + + void procFirstSegment(std::vector& buf, size_t offset) { + for (size_t i = 0; i < buf.size(); i++) { + buf[i] ^= key[getSegmentKey(offset + i)]; + } + } + + void procASegment(std::vector& buf, size_t offset) { + // Initialise a new seed box + std::vector 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(); + } +}; diff --git a/qmc_key.hpp b/qmc_key.hpp new file mode 100644 index 0000000..f3178cd --- /dev/null +++ b/qmc_key.hpp @@ -0,0 +1,217 @@ +#include"TencentTea.hpp" +#include "base64.hpp" + +void simpleMakeKey(uint8_t salt, int length, std::vector &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 v2KeyPrefix = { 0x51, 0x51, 0x4D, 0x75, 0x73, 0x69, 0x63, 0x20, 0x45, 0x6E, 0x63, 0x56, 0x32, 0x2C, 0x4B, 0x65, 0x79, 0x3A }; + +bool decryptV2Key(std::vector key, std::vector& 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 mixKey1 = { 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 }; + std::vector mixKey2 = { 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 }; + + std::vector out; + std::vector 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 key, std::vector& outVec) +{ + if (key.size() < 16) + { + outVec.resize(0); + //EncV2 key size is too small. + return false; + } + + std::vector 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 mixKey1 = { 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 }; + std::vector mixKey2 = { 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 }; + + std::vector 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 raw, std::vector &outVec) { + std::vector 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 tmpIn = rawDec; + if (!decryptV2Key(tmpIn, rawDec)) + { + //decrypt EncV2 failed. + return false; + } + + std::vector simpleKey; + simpleKey.resize(8); + simpleMakeKey(106, 8, simpleKey); + std::vector 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 out; + std::vector 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 raw, std::vector& outVec, bool useEncV2 = true) { + std::vector simpleKey; + simpleKey.resize(8); + simpleMakeKey(106, 8, simpleKey); + std::vector 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 out; + out.resize(raw.size() - 8); + for (size_t i = 0; i < out.size(); i++) + { + out[i] = raw[8 + i]; + } + std::vector 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 tmpIn = raw; + if (!encryptV2Key(tmpIn, raw)) + { + //encrypt EncV2 failed. + return false; + } + } + + std::vector 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; + } +} diff --git a/qmc_wasm.ts b/qmc_wasm.ts new file mode 100644 index 0000000..f747bf4 --- /dev/null +++ b/qmc_wasm.ts @@ -0,0 +1,74 @@ +import { QmcCrypto } from '@xhacker/qmcwasm/QmcWasmBundle'; +import QmcCryptoModule from '@xhacker/qmcwasm/QmcWasmBundle'; +import { MergeUint8Array } from '@/utils/MergeUint8Array'; + +// 每次处理 2M 的数据 +const DECRYPTION_BUF_SIZE = 2 *1024 * 1024; + +export interface QMCDecryptionResult { + success: boolean; + data: Uint8Array; + songId: string | number; + error: string; +} + +/** + * 解密一个 QMC 加密的文件。 + * + * 如果检测并解密成功,返回解密后的 Uint8Array 数据。 + * @param {ArrayBuffer} qmcBlob 读入的文件 Blob + */ +export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise { + const result: QMCDecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' }; + + // 初始化模组 + let QmcCryptoObj: QmcCrypto; + + try { + QmcCryptoObj = await QmcCryptoModule(); + } catch (err: any) { + result.error = err?.message || 'wasm 加载失败'; + return result; + } + if (!QmcCryptoObj) { + result.error = 'wasm 加载失败'; + return result; + } + + // 申请内存块,并文件末端数据到 WASM 的内存堆 + const qmcBuf = new Uint8Array(qmcBlob); + const pQmcBuf = QmcCryptoObj._malloc(DECRYPTION_BUF_SIZE); + QmcCryptoObj.writeArrayToMemory(qmcBuf.slice(-DECRYPTION_BUF_SIZE), pQmcBuf); + + // 进行解密初始化 + ext = '.' + ext; + const tailSize = QmcCryptoObj.preDec(pQmcBuf, DECRYPTION_BUF_SIZE, ext); + if (tailSize == -1) { + result.error = QmcCryptoObj.getErr(); + return result; + } else { + result.songId = QmcCryptoObj.getSongId(); + result.songId = result.songId == "0" ? 0 : result.songId; + } + + const decryptedParts = []; + let offset = 0; + let bytesToDecrypt = qmcBuf.length - tailSize; + while (bytesToDecrypt > 0) { + const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE); + + // 解密一些片段 + const blockData = new Uint8Array(qmcBuf.slice(offset, offset + blockSize)); + QmcCryptoObj.writeArrayToMemory(blockData, pQmcBuf); + decryptedParts.push(QmcCryptoObj.HEAPU8.slice(pQmcBuf, pQmcBuf + QmcCryptoObj.decBlob(pQmcBuf, blockSize, offset))); + + offset += blockSize; + bytesToDecrypt -= blockSize; + } + QmcCryptoObj._free(pQmcBuf); + + result.data = MergeUint8Array(decryptedParts); + result.success = true; + + return result; +}