From 2a462c2eff32dbeac1499e544f9d0378c99f9939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Thu, 17 Oct 2024 09:50:14 +0900 Subject: [PATCH] inital commit --- .clang-format | 5 + .editorconfig | 14 ++ .gitignore | 3 + .idea/.gitignore | 8 + .idea/codeStyles/Project.xml | 134 +++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/editor.xml | 341 +++++++++++++++++++++++++++ .idea/kgg-dec.iml | 2 + .idea/misc.xml | 13 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + CMakeLists.txt | 37 +++ CMakePresets.json | 67 ++++++ Jenkinsfile | 44 ++++ README.MD | 22 ++ Usage.zh.txt | 18 ++ src/common/base64.cpp | 89 +++++++ src/common/base64.h | 7 + src/common/endian_helper.h | 50 ++++ src/infra/infra.cpp | 99 ++++++++ src/infra/infra.h | 26 ++ src/infra/sqlite_base.h | 9 + src/infra/sqlite_error.h | 99 ++++++++ src/infra/sqlite_fn.h | 71 ++++++ src/main.cpp | 145 ++++++++++++ src/qmc2/ekey.cpp | 69 ++++++ src/qmc2/ekey.h | 6 + src/qmc2/qmc2.h | 54 +++++ src/qmc2/qmc2_factory.cpp | 20 ++ src/qmc2/qmc2_map.cpp | 24 ++ src/qmc2/qmc2_rc4.cpp | 127 ++++++++++ src/tc_tea/tc_tea.cpp | 70 ++++++ src/tc_tea/tc_tea.h | 19 ++ src/tc_tea/tea_ecb.h | 40 ++++ 34 files changed, 1751 insertions(+) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/editor.xml create mode 100644 .idea/kgg-dec.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 Jenkinsfile create mode 100644 README.MD create mode 100644 Usage.zh.txt create mode 100644 src/common/base64.cpp create mode 100644 src/common/base64.h create mode 100644 src/common/endian_helper.h create mode 100644 src/infra/infra.cpp create mode 100644 src/infra/infra.h create mode 100644 src/infra/sqlite_base.h create mode 100644 src/infra/sqlite_error.h create mode 100644 src/infra/sqlite_fn.h create mode 100644 src/main.cpp create mode 100644 src/qmc2/ekey.cpp create mode 100644 src/qmc2/ekey.h create mode 100644 src/qmc2/qmc2.h create mode 100644 src/qmc2/qmc2_factory.cpp create mode 100644 src/qmc2/qmc2_map.cpp create mode 100644 src/qmc2/qmc2_rc4.cpp create mode 100644 src/tc_tea/tc_tea.cpp create mode 100644 src/tc_tea/tc_tea.h create mode 100644 src/tc_tea/tea_ecb.h diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..4690a67 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +BasedOnStyle: Chromium +IndentWidth: 4 +TabWidth: 4 +InsertNewlineAtEOF: true +ColumnLimit: 120 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8a2eb26 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.{json,yml,md}] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23628bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +.cache/ +cmake-build-* diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..6ddeda2 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,134 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..9e78c19 --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,341 @@ + + + + + \ No newline at end of file diff --git a/.idea/kgg-dec.iml b/.idea/kgg-dec.iml new file mode 100644 index 0000000..f08604b --- /dev/null +++ b/.idea/kgg-dec.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5b873ea --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ee66348 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ccf1ae2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.10) + +project(kgg-dec LANGUAGES CXX) + +include(CheckIPOSupported) +check_ipo_supported(RESULT lto_supported OUTPUT error) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(kgg-dec + src/main.cpp + src/common/base64.cpp + src/infra/infra.cpp + src/tc_tea/tc_tea.cpp + src/qmc2/ekey.cpp + src/qmc2/qmc2_factory.cpp + src/qmc2/qmc2_map.cpp + src/qmc2/qmc2_rc4.cpp +) + +target_include_directories(kgg-dec + PRIVATE + src/common + src/tc_tea +) + +target_link_libraries(kgg-dec PRIVATE shell32 ole32) + +if (lto_supported) + set_property(TARGET kgg-dec PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) +endif() + +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -s -fdata-sections -ffunction-sections -flto -fmerge-all-constants -fno-exceptions -fno-rtti") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -s -Wl,--gc-sections -flto") +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..bf212c3 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,67 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 19, + "patch": 0 + }, + "configurePresets": [ + { + "name": "default", + "hidden": true, + "generator": "Ninja", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "mingw", + "inherits": "default", + "description": "Configure for MinGW", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/mingw", + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Windows", + "CMAKE_C_COMPILER": "i686-w64-mingw32-gcc-win32", + "CMAKE_CXX_COMPILER": "i686-w64-mingw32-g++-win32", + "CMAKE_FIND_LIBRARY_SUFFIXES": ".a", + "BUILD_SHARED_LIBS": "OFF", + "CMAKE_EXE_LINKER_FLAGS": "-static -static-libgcc -static-libstdc++ -lucrt" + } + }, + { + "name": "vs", + "inherits": "default", + "description": "Configure for Visual Studio", + "generator": "Visual Studio 17 2022", + "binaryDir": "${sourceDir}/build/vs2022", + "architecture": "Win32" + } + ], + "buildPresets": [ + { + "name": "mingw-debug", + "configurePreset": "mingw", + "description": "Build using MinGW (Debug)", + "configuration": "Debug" + }, + { + "name": "mingw-release", + "configurePreset": "mingw", + "description": "Build using MinGW (Release)", + "configuration": "Release" + }, + { + "name": "vs-debug", + "configurePreset": "vs", + "description": "Build using VS (Debug)", + "configuration": "Debug" + }, + { + "name": "vs-release", + "configurePreset": "vs", + "description": "Build using VS (Release)", + "configuration": "Release" + } + ] +} diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..48b2a82 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,44 @@ +properties([ + buildDiscarder( + logRotator( + artifactDaysToKeepStr: '', + artifactNumToKeepStr: '', + daysToKeepStr: '', + numToKeepStr: '3' + ) + ) +]) + +pipeline { + options { timestamps () } + agent { + label 'vs2022 && windows' + } + + stages { + stage('Build') { + steps { + bat ''' + cmake --preset vs -DCMAKE_BUILD_TYPE=Release + cmake --build --preset vs-release --config Release + copy /y README.MD .\\build\\vs2022\\Release\\ + copy /y Usage*.txt .\\build\\vs2022\\Release\\ + ''' + } + } + + stage('Archive') { + steps { + bat ''' + 7za a -tzip -mtm- build/kgg-dec.zip .\\build\\vs2022\\Release\\* + ''' + } + + post { + success { + archiveArtifacts 'build/*.zip' + } + } + } + } +} diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..213896f --- /dev/null +++ b/README.MD @@ -0,0 +1,22 @@ +# kgg-dec + +Kugou `kgg` decryption tool. + +## Build + +Build in Windows with CMake + Visual Studio 2022 + +```cmd +cmake --preset vs -DCMAKE_BUILD_TYPE=Release +cmake --build --preset vs-release --config Release +copy /y README.MD .\\build\\vs2022\\ +``` + +## Usage + +1. Copy `infra.dll` from KuGou installation directory to where `kgg-dec.exe` is located. +2. Invoke exe with path to the directory containing kgg file. + + ```shell + kgg-dec.exe . + ``` diff --git a/Usage.zh.txt b/Usage.zh.txt new file mode 100644 index 0000000..c7eb512 --- /dev/null +++ b/Usage.zh.txt @@ -0,0 +1,18 @@ +# KGG 解密工具 by LSR + +项目地址:https://git.unlock-music.dev/um/kgg-dec + +使用方法 (命令行): + +1. 从酷狗安装目录拷贝 `infra.dll` 文件到 `kgg-dec.exe` 的目录。 +2. 启动 `kgg-dec.exe`,其中第一个参数为含有 `*.kgg` 文件的目录。 + +或者: + +1. 将 `kgg-dec.exe` 与 `infra.dll` 拷贝到 KGG 所在目录。 +2. 双击 `kgg-dec.exe`,开始解密当前目录。 + +错误排查: + +1. 需要至少播放一次 KGG 文件,并确保酷狗能正常播放。 +2. 文件后缀名嗅探代码只支持:`ogg` / `flac`,其他格式默认回退到 `mp3`。 diff --git a/src/common/base64.cpp b/src/common/base64.cpp new file mode 100644 index 0000000..b5dace7 --- /dev/null +++ b/src/common/base64.cpp @@ -0,0 +1,89 @@ +#include +#include +#include +#include +#include + +constexpr static auto kBase64Table = ([]() { + const char* table_str = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "+/"; + std::array table{}; + for (auto& item : table) { + item = *table_str++; + } + return table; +})(); + +// NOLINTBEGIN(*-magic-numbers) + +constexpr static auto kBase64ReverseTable = ([]() { + std::array reverse_table{}; + + for (size_t i = 0; i < kBase64Table.size(); i++) { + reverse_table[kBase64Table[i]] = static_cast(i); + } + + // url-safe varient + reverse_table['-'] = 62; + reverse_table['_'] = 63; + + return reverse_table; +})(); + +inline size_t b64_decode_buffer_len(size_t len) { + // Every 4 bytes in, it will yield 3 bytes output + return (len + 3) / 4 * 3; +} + +inline size_t b64_decode(uint8_t* output, const uint8_t* input, size_t input_len) { + auto* p_out = output; + size_t total_decoded = 0; + + auto decode_block = [&p_out](const uint8_t* p_in) { + // NOLINTBEGIN(*-identifier-length) + uint8_t a{kBase64ReverseTable[p_in[0]]}; + uint8_t b{kBase64ReverseTable[p_in[1]]}; + uint8_t c{kBase64ReverseTable[p_in[2]]}; + uint8_t d{kBase64ReverseTable[p_in[3]]}; + // NOLINTEND(*-identifier-length) + + *p_out++ = (a << 2) | (b >> 4); + *p_out++ = (b << 4) | (c >> 2); + *p_out++ = (c << 6) | (d >> 0); + + if (p_in[2] == '=') { + p_out -= 2; + return true; + } + + if (p_in[3] == '=') { + p_out -= 1; + return true; + } + + return false; + }; + + for (const auto* p_input_end = input + input_len - 4; input <= p_input_end; input += 4) { + if (decode_block(input)) { + return p_out - output; + } + } + + input_len %= 4; + if (input_len != 0) { + std::array buffer{0, '=', '=', '='}; + std::copy_n(input, input_len, buffer.begin()); + decode_block(buffer.data()); + } + return p_out - output; +} + +std::vector b64_decode(const uint8_t* input, size_t len) { + std::vector result(b64_decode_buffer_len(len)); + result.resize(b64_decode(result.data(), input, len)); + return result; +} diff --git a/src/common/base64.h b/src/common/base64.h new file mode 100644 index 0000000..54e92f2 --- /dev/null +++ b/src/common/base64.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include +#include + +std::vector b64_decode(const uint8_t *input, size_t len); diff --git a/src/common/endian_helper.h b/src/common/endian_helper.h new file mode 100644 index 0000000..fcd77bf --- /dev/null +++ b/src/common/endian_helper.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +#if defined(_MSC_VER) +#define bswap_u16 _byteswap_ushort +#define bswap_u32 _byteswap_ulong +#define bswap_u64 _byteswap_uint64 +#else +#define bswap_u16 __builtin_bswap16 +#define bswap_u32 __builtin_bswap32 +#define bswap_u64 __builtin_bswap64 +#endif + +namespace Endian { + +inline uint64_t be_u64_read(const uint8_t* p) { + if constexpr (std::endian::native == std::endian::big) { + return *reinterpret_cast(p); + } else { + return bswap_u64(*reinterpret_cast(p)); + } +} + +inline void be_u64_write(uint8_t* p, uint64_t value) { + if constexpr (std::endian::native == std::endian::big) { + *reinterpret_cast(p) = value; + } else { + *reinterpret_cast(p) = bswap_u64(value); + } +} + +inline uint32_t be_u32_read(const uint8_t* p) { + if constexpr (std::endian::native == std::endian::big) { + return *reinterpret_cast(p); + } else { + return bswap_u32(*reinterpret_cast(p)); + } +} + +inline void be_u32_write(uint8_t* p, uint32_t value) { + if constexpr (std::endian::native == std::endian::big) { + *reinterpret_cast(p) = value; + } else { + *reinterpret_cast(p) = bswap_u32(value); + } +} + +} // namespace Endian diff --git a/src/infra/infra.cpp b/src/infra/infra.cpp new file mode 100644 index 0000000..42e2a0c --- /dev/null +++ b/src/infra/infra.cpp @@ -0,0 +1,99 @@ +#include "infra.h" +#include "sqlite_error.h" +#include "sqlite_fn.h" + +#include + +namespace Infra { + +HMODULE g_mod_infra_dll; +sqlite3_open_v2_t sqlite3_open_v2; +sqlite3_key_t sqlite3_key; +sqlite3_prepare_v2_t sqlite3_prepare_v2; +sqlite3_step_t sqlite3_step; +sqlite3_column_text_t sqlite3_column_text; +sqlite3_close_v2_t sqlite3_close_v2; +sqlite3_finalize_t sqlite3_finalize; + +bool g_init_sqlite_ok = []() { + g_mod_infra_dll = LoadLibraryA("infra.dll"); + if (g_mod_infra_dll == nullptr) { + return false; + } + + sqlite3_open_v2 = reinterpret_cast(GetProcAddress(g_mod_infra_dll, "sqlite3_open_v2")); + sqlite3_key = reinterpret_cast(GetProcAddress(g_mod_infra_dll, "sqlite3_key")); + sqlite3_prepare_v2 = reinterpret_cast(GetProcAddress(g_mod_infra_dll, "sqlite3_prepare_v2")); + sqlite3_step = reinterpret_cast(GetProcAddress(g_mod_infra_dll, "sqlite3_step")); + sqlite3_column_text = + reinterpret_cast(GetProcAddress(g_mod_infra_dll, "sqlite3_column_text")); + sqlite3_close_v2 = reinterpret_cast(GetProcAddress(g_mod_infra_dll, "sqlite3_close_v2")); + sqlite3_finalize = reinterpret_cast(GetProcAddress(g_mod_infra_dll, "sqlite3_finalize")); + + return sqlite3_open_v2 && sqlite3_key && sqlite3_prepare_v2 && sqlite3_step && sqlite3_column_text && + sqlite3_close_v2 && sqlite3_finalize; +}(); + +SQLite::SQLite(const std::filesystem::path& db_path) { + ok_ = false; + int rc{-1}; + if (!g_init_sqlite_ok) { + return; + } + + auto db_path_u8 = db_path.generic_u8string(); + rc = sqlite3_open_v2(reinterpret_cast(db_path_u8.c_str()), &db_, SQLITE_OPEN_READONLY, nullptr); + if (rc != SQLITE_OK) { + return; + } + + rc = sqlite3_key(db_, "7777B48756BA491BB4CEE771A3E2727E", 0x20); + if (rc != SQLITE_OK) { + return; + } + + ok_ = true; +} + +kgm_ekey_db_t SQLite::dump_ekey(int& error) { + if (ok_ == false) { + error = SQLITE_ERROR; + return {}; + } + + int rc{-1}; + sqlite3_stmt* stmt{nullptr}; + + rc = sqlite3_prepare_v2(db_, + "select EncryptionKeyId, EncryptionKey from ShareFileItems" + " where EncryptionKey != ''", + -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + error = rc; + return {}; + } + + kgm_ekey_db_t result{}; + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + const auto* ekey_id = reinterpret_cast(sqlite3_column_text(stmt, 0)); + const auto* ekey = reinterpret_cast(sqlite3_column_text(stmt, 1)); + result[ekey_id] = ekey; + } + + if (rc != SQLITE_DONE) { + error = rc; + } + + sqlite3_finalize(stmt); + error = 0; + return result; +} + +SQLite::~SQLite() { + if (db_ != nullptr) { + sqlite3_close_v2(db_); + db_ = nullptr; + } +} + +} // namespace Infra diff --git a/src/infra/infra.h b/src/infra/infra.h new file mode 100644 index 0000000..41dbece --- /dev/null +++ b/src/infra/infra.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include "sqlite_base.h" + +namespace Infra { + +typedef std::unordered_map kgm_ekey_db_t; +extern bool g_init_sqlite_ok; + +class SQLite { + public: + explicit SQLite(const std::filesystem::path& db_path); + ~SQLite(); + + [[nodiscard]] bool is_ok() const { return ok_; } + kgm_ekey_db_t dump_ekey(int& error); + + private: + bool ok_ = false; + sqlite3* db_{nullptr}; +}; + +} // namespace Infra diff --git a/src/infra/sqlite_base.h b/src/infra/sqlite_base.h new file mode 100644 index 0000000..6bf48b6 --- /dev/null +++ b/src/infra/sqlite_base.h @@ -0,0 +1,9 @@ +#pragma once + +// SQLite +#define SQLITE_OPEN_READONLY 0x00000001 /* Ok for sqlite3_open_v2() */ +#define SQLITE_OPEN_READWRITE 0x00000002 /* Ok for sqlite3_open_v2() */ +#define SQLITE_OPEN_CREATE 0x00000004 /* Ok for sqlite3_open_v2() */ + +typedef struct sqlite3 sqlite3; +typedef struct sqlite3_stmt sqlite3_stmt; diff --git a/src/infra/sqlite_error.h b/src/infra/sqlite_error.h new file mode 100644 index 0000000..ec2e495 --- /dev/null +++ b/src/infra/sqlite_error.h @@ -0,0 +1,99 @@ +#pragma once + +#define SQLITE_OK (0) + +/* beginning-of-error-codes */ +#define SQLITE_ERROR 1 /* Generic error */ +#define SQLITE_INTERNAL 2 /* Internal logic error in SQLite */ +#define SQLITE_PERM 3 /* Access permission denied */ +#define SQLITE_ABORT 4 /* Callback routine requested an abort */ +#define SQLITE_BUSY 5 /* The database file is locked */ +#define SQLITE_LOCKED 6 /* A table in the database is locked */ +#define SQLITE_NOMEM 7 /* A malloc() failed */ +#define SQLITE_READONLY 8 /* Attempt to write a readonly database */ +#define SQLITE_INTERRUPT 9 /* Operation terminated by sqlite3_interrupt()*/ +#define SQLITE_IOERR 10 /* Some kind of disk I/O error occurred */ +#define SQLITE_CORRUPT 11 /* The database disk image is malformed */ +#define SQLITE_NOTFOUND 12 /* Unknown opcode in sqlite3_file_control() */ +#define SQLITE_FULL 13 /* Insertion failed because database is full */ +#define SQLITE_CANTOPEN 14 /* Unable to open the database file */ +#define SQLITE_PROTOCOL 15 /* Database lock protocol error */ +#define SQLITE_EMPTY 16 /* Internal use only */ +#define SQLITE_SCHEMA 17 /* The database schema changed */ +#define SQLITE_TOOBIG 18 /* String or BLOB exceeds size limit */ +#define SQLITE_CONSTRAINT 19 /* Abort due to constraint violation */ +#define SQLITE_MISMATCH 20 /* Data type mismatch */ +#define SQLITE_MISUSE 21 /* Library used incorrectly */ +#define SQLITE_NOLFS 22 /* Uses OS features not supported on host */ +#define SQLITE_AUTH 23 /* Authorization denied */ +#define SQLITE_FORMAT 24 /* Not used */ +#define SQLITE_RANGE 25 /* 2nd parameter to sqlite3_bind out of range */ +#define SQLITE_NOTADB 26 /* File opened that is not a database file */ +#define SQLITE_NOTICE 27 /* Notifications from sqlite3_log() */ +#define SQLITE_WARNING 28 /* Warnings from sqlite3_log() */ +#define SQLITE_ROW 100 /* sqlite3_step() has another row ready */ +#define SQLITE_DONE 101 /* sqlite3_step() has finished executing */ +/* end-of-error-codes */ + +inline const char* sqlite_get_error(int rc) { + switch (rc) { + case SQLITE_ERROR: + return "SQLITE_ERROR: Generic error"; + case SQLITE_INTERNAL: + return "SQLITE_INTERNAL: Internal logic error in SQLite"; + case SQLITE_PERM: + return "SQLITE_PERM: Access permission denied"; + case SQLITE_ABORT: + return "SQLITE_ABORT: Callback routine requested an abort"; + case SQLITE_BUSY: + return "SQLITE_BUSY: The database file is locked"; + case SQLITE_LOCKED: + return "SQLITE_LOCKED: A table in the database is locked"; + case SQLITE_NOMEM: + return "SQLITE_NOMEM: A malloc() failed"; + case SQLITE_READONLY: + return "SQLITE_READONLY: Attempt to write a readonly database"; + case SQLITE_INTERRUPT: + return "SQLITE_INTERRUPT: Operation terminated by sqlite3_interrupt()"; + case SQLITE_IOERR: + return "SQLITE_IOERR: Some kind of disk I/O error occurred"; + case SQLITE_CORRUPT: + return "SQLITE_CORRUPT: The database disk image is malformed"; + case SQLITE_NOTFOUND: + return "SQLITE_NOTFOUND: Unknown opcode in sqlite3_file_control()"; + case SQLITE_FULL: + return "SQLITE_FULL: Insertion failed because database is full"; + case SQLITE_CANTOPEN: + return "SQLITE_CANTOPEN: Unable to open the database file"; + case SQLITE_PROTOCOL: + return "SQLITE_PROTOCOL: Database lock protocol error"; + case SQLITE_EMPTY: + return "SQLITE_EMPTY: Internal use only"; + case SQLITE_SCHEMA: + return "SQLITE_SCHEMA: The database schema changed"; + case SQLITE_TOOBIG: + return "SQLITE_TOOBIG: String or BLOB exceeds size limit"; + case SQLITE_CONSTRAINT: + return "SQLITE_CONSTRAINT: Abort due to constraint violation"; + case SQLITE_MISMATCH: + return "SQLITE_MISMATCH: Data type mismatch"; + case SQLITE_MISUSE: + return "SQLITE_MISUSE: Library used incorrectly"; + case SQLITE_NOLFS: + return "SQLITE_NOLFS: Uses OS features not supported on host"; + case SQLITE_AUTH: + return "SQLITE_AUTH: Authorization denied"; + case SQLITE_FORMAT: + return "SQLITE_FORMAT: Not used"; + case SQLITE_RANGE: + return "SQLITE_RANGE: 2nd parameter to sqlite3_bind out of range"; + case SQLITE_NOTADB: + return "SQLITE_NOTADB: File opened that is not a database file"; + case SQLITE_NOTICE: + return "SQLITE_NOTICE: Notifications from sqlite3_log()"; + case SQLITE_WARNING: + return "SQLITE_WARNING: Warnings from sqlite3_log()"; + default: + return ""; + } +} diff --git a/src/infra/sqlite_fn.h b/src/infra/sqlite_fn.h new file mode 100644 index 0000000..45fed18 --- /dev/null +++ b/src/infra/sqlite_fn.h @@ -0,0 +1,71 @@ +#pragma once + +#include "sqlite_base.h" + +/** + * @brief Opens a SQLite database file with extended options. + * + * @param filename The name of the database file to be opened (UTF-8 encoded). + * @param ppDb A pointer to a pointer that will receive the SQLite database + * handle upon successful opening. + * @param flags Flags that control the behavior of the database connection. + * @param zVfs The name of the VFS (Virtual File System) module to use. + * If NULL, the default VFS is used. + * @return Returns SQLITE_OK on success or an error code on failure. + */ +typedef int (*sqlite3_open_v2_t)(const char* filename, sqlite3** ppDb, int flags, const char* zVfs); + +/** + * @brief Compiles an SQL statement into a prepared statement. + * + * @param db Database handle. + * @param zSql SQL statement, UTF-8 encoded. + * @param n Maximum length of zSql in bytes. + * @param ppStmt OUT: Statement handle. + * @param pzTail OUT: Pointer to unused portion of zSql. + * @return Returns SQLITE_OK on success or an error code on failure. + */ +typedef int (*sqlite3_prepare_v2_t)(sqlite3* db, const char* zSql, int n, sqlite3_stmt** ppStmt, const char** pzTail); + +/** + * @brief Evaluates a prepared statement. + * + * @param stmt Prepared statement. + * @return Returns SQLITE_ROW, SQLITE_DONE, or an error code. + */ +typedef int (*sqlite3_step_t)(sqlite3_stmt* stmt); + +/** + * @brief Returns the text value of a column in the current row of a result set. + * + * @param stmt Prepared statement. + * @param iCol Column index. + * @return Text value of the column. + */ +typedef const unsigned char* (*sqlite3_column_text_t)(sqlite3_stmt* stmt, int iCol); + +/** + * @brief Destroys a prepared statement object. + * + * @param stmt Prepared statement. + * @return Returns SQLITE_OK on success or an error code on failure. + */ +typedef int (*sqlite3_finalize_t)(sqlite3_stmt* stmt); + +/** + * @brief Closes a database connection and invalidates all prepared statements. + * + * @param db Database handle. + * @return Returns SQLITE_OK on success or an error code on failure. + */ +typedef int (*sqlite3_close_v2_t)(sqlite3* db); + +/** + * @brief Sets the encryption key for a database. + * + * @param db Database to be keyed. + * @param pKey The key. + * @param nKey The length of the key in bytes. + * @return Returns SQLITE_OK on success or an error code on failure. + */ +typedef int (*sqlite3_key_t)(sqlite3* db, const void* pKey, int nKey); diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..fb97864 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,145 @@ +// clang-format off +#include +#include +#include +#include +// clang-format on + +#include +#include +#include +#include +#include +#include + +#include "infra/infra.h" +#include "infra/sqlite_error.h" +#include "qmc2/qmc2.h" + +using Infra::kgm_ekey_db_t; + +constexpr std::array kMagicHeader{0x7C, 0xD5, 0x32, 0xEB, 0x86, 0x02, 0x7F, 0x4B, + 0xA8, 0xAF, 0xA6, 0x8E, 0x0F, 0xFF, 0x99, 0x14}; + +void DecryptKGG(const kgm_ekey_db_t& ekey_db, const std::filesystem::path& kgg_file) { + auto kgg_fname = kgg_file.filename().wstring(); + fwprintf(stderr, L"[INFO] reading %s...\n", kgg_fname.c_str()); + + std::ifstream ifs_kgg(kgg_file, std::ios::binary); + char header[0x100]{}; + ifs_kgg.read(header, sizeof(ifs_kgg)); + if (std::equal(kMagicHeader.cbegin(), kMagicHeader.cend(), header)) { + fprintf(stderr, "[WARN] invalid kgg header, skip.\n"); + return; + } + uint32_t offset_to_audio = *reinterpret_cast(&header[0x10]); + uint32_t encrypt_mode = *reinterpret_cast(&header[0x14]); + if (encrypt_mode != 5) { + fprintf(stderr, "[WARN] invalid enc_version (expect=0x05, got 0x%02x), skip.\n", encrypt_mode); + return; + } + uint32_t audio_hash_len = *reinterpret_cast(&header[0x44]); + if (audio_hash_len != 0x20) { + fprintf(stderr, "[WARN] audio hash length invalid (expect=0x20, got 0x%02x), skip.\n", audio_hash_len); + return; + } + std::string audio_hash(&header[0x48], &header[0x48 + audio_hash_len]); + std::string ekey{}; + if (auto it = ekey_db.find(audio_hash); it != ekey_db.end()) { + ekey = it->second; + } else { + fprintf(stderr, "[WARN] ekey not found, skip.\n"); + return; + } + + auto qmc2 = QMC2::Create(ekey); + if (!qmc2) { + fprintf(stderr, "[WARN] create qmc2 failed, skip.\n"); + return; + } + + ifs_kgg.seekg(offset_to_audio, std::ios::beg); + std::vector buffer(1024 * 1024); + ifs_kgg.read(reinterpret_cast(buffer.data()), buffer.size()); + auto n = static_cast(ifs_kgg.gcount()); + qmc2->Decrypt(std::span(buffer.begin(), n), 0); + + auto decrypted_path = kgg_file; + auto magic = std::span(buffer.cbegin(), 4); + if (std::equal(magic.begin(), magic.end(), "fLaC")) { + decrypted_path.replace_filename(decrypted_path.stem().wstring() + L"_kgg-dec.flac"); + } else if (std::equal(magic.begin(), magic.end(), "OggS")) { + decrypted_path.replace_filename(decrypted_path.stem().wstring() + L"_kgg-dec.ogg"); + } else { + decrypted_path.replace_filename(decrypted_path.stem().wstring() + L"_kgg-dec.mp3"); + } + if (exists(decrypted_path)) { + fprintf(stderr, "[WARN] output file '%s' exists, skip.\n", decrypted_path.filename().string().c_str()); + return; + } + + std::ofstream ofs_decrypted(decrypted_path, std::ios::binary); + ofs_decrypted.write(reinterpret_cast(buffer.data()), n); + size_t offset{n}; + while (!ifs_kgg.eof()) { + ifs_kgg.read(reinterpret_cast(buffer.data()), buffer.size()); + n = static_cast(ifs_kgg.gcount()); + qmc2->Decrypt(std::span(buffer.begin(), n), offset); + ofs_decrypted.write(reinterpret_cast(buffer.data()), n); + offset += n; + } + + auto decrypted_fname = decrypted_path.filename().wstring(); + fwprintf(stderr, L"[INFO] OK! kgg --> %s\n", decrypted_fname.c_str()); +} + +void WalkFileOrDir(const kgm_ekey_db_t& ekey_db, const std::filesystem::path& input_path) { + if (is_directory(input_path)) { + for (auto const& dir_entry : std::filesystem::directory_iterator{input_path}) { + DecryptKGG(ekey_db, dir_entry.path()); + } + } else if (is_regular_file(input_path)) { + DecryptKGG(ekey_db, input_path); + } +} + +int main() { + SetConsoleOutputCP(CP_UTF8); + setlocale(LC_ALL, ".UTF8"); + + fputs("kgg-dec v0.1 by LSR\n", stderr); + fputs("Usage: kgg-dec [kgg-dir=.]\n\n", stderr); + PWSTR pAppDirPath{}; + SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &pAppDirPath); + std::filesystem::path kgmPath{pAppDirPath}; + CoTaskMemFree(pAppDirPath); + kgmPath = kgmPath / L"Kugou8" / L"KGMusicV3.db"; + + int error{-1}; + Infra::SQLite db{kgmPath}; + if (!db.is_ok()) { + fprintf(stderr, "[ERR ] db init error: is infra.dll ok?\n"); + return 1; + } + auto ekey_db = db.dump_ekey(error); + if (error != 0) { + fprintf(stderr, "[ERR ] dump ekey failed %d (%s)", error, sqlite_get_error(error)); + return 1; + } + +#ifdef _DEBUG + printf("ekey_db:\n"); + for (auto& [a, b] : ekey_db) { + printf("%s --> %s\n", a.c_str(), b.c_str()); + } +#endif + + int argc; + wchar_t** argv = CommandLineToArgvW(GetCommandLineW(), &argc); + + std::filesystem::path input_dir{(argc < 2) ? L"." : argv[1]}; + input_dir = absolute(input_dir); + WalkFileOrDir(ekey_db, input_dir); + + return 0; +} diff --git a/src/qmc2/ekey.cpp b/src/qmc2/ekey.cpp new file mode 100644 index 0000000..fbc2857 --- /dev/null +++ b/src/qmc2/ekey.cpp @@ -0,0 +1,69 @@ +#include "ekey.h" +#include "base64.h" +#include "tc_tea.h" + +#include +#include +#include +#include + +const static std::string kEKeyV2Prefix = "UVFNdXNpYyBFbmNWMixLZXk6"; +const static std::array kEKeyV2Key1{ + 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, + 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28, +}; +const static std::array kEKeyV2Key2{ + 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, + 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54, +}; + +template std::span ss2span(std::string_view sv) { + auto *data = reinterpret_cast(sv.data()); + return std::span(const_cast(data), sv.size()); +} + +template std::string_view span2ss(std::span span) { + return std::string_view(reinterpret_cast(span.data()), span.size()); +} + +void remove_trailing_zeros(std::vector &vec) { + auto it = std::find_if(vec.rbegin(), vec.rend(), + [](uint8_t value) { return value != 0; }); + vec.erase(it.base(), vec.end()); +} + +std::vector decrypt_ekey_v1(std::string_view ekey) { + std::vector result = + b64_decode(reinterpret_cast(ekey.data()), ekey.size()); + remove_trailing_zeros(result); + + uint32_t tea_key[4] = { + 0x69005600 | static_cast(result[0] << 16) | (result[1]), + 0x46003800 | static_cast(result[2] << 16) | (result[3]), + 0x2b002000 | static_cast(result[4] << 16) | (result[5]), + 0x15000b00 | static_cast(result[6] << 16) | (result[7]), + }; + auto decrypted = tc_tea_cbc_decrypt(std::span(result).subspan(8), tea_key); + if (decrypted.empty()) { + return {}; + } + result.resize(8); + result.insert(result.end(), decrypted.begin(), decrypted.end()); + return result; +} + +std::vector decrypt_ekey_v2(std::string_view ekey) { + std::vector result; + result = tc_tea_cbc_decrypt(ss2span(ekey), kEKeyV2Key1.data()); + result = tc_tea_cbc_decrypt(std::span(result), kEKeyV2Key2.data()); + return decrypt_ekey_v1(span2ss(std::span(result))); +} + +std::vector decrypt_ekey(std::string_view ekey) { + if (ekey.starts_with(kEKeyV2Prefix)) { + ekey.remove_prefix(kEKeyV2Prefix.size()); + return decrypt_ekey_v2(ekey); + } + + return decrypt_ekey_v1(ekey); +} diff --git a/src/qmc2/ekey.h b/src/qmc2/ekey.h new file mode 100644 index 0000000..449bebd --- /dev/null +++ b/src/qmc2/ekey.h @@ -0,0 +1,6 @@ +#pragma once +#include +#include +#include + +std::vector decrypt_ekey(std::string_view ekey); diff --git a/src/qmc2/qmc2.h b/src/qmc2/qmc2.h new file mode 100644 index 0000000..eb2fab3 --- /dev/null +++ b/src/qmc2/qmc2.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace QMC2 { + +class QMC2_Base { + public: + QMC2_Base(){}; + virtual ~QMC2_Base() = default; + + virtual void Decrypt(std::span data, size_t offset) = 0; +}; + +constexpr size_t kMapIndexOffset = 71214; +constexpr size_t kMapKeySize = 128; + +class QMC2_MAP : public QMC2_Base { + public: + explicit QMC2_MAP(std::span key); + + void Decrypt(std::span data, size_t offset) override; + + private: + std::array key_{}; +}; + +constexpr size_t kFirstSegmentSize = 0x0080; +constexpr size_t kOtherSegmentSize = 0x1400; +constexpr size_t kRC4StreamSize = kOtherSegmentSize + 512; + +class QMC2_RC4 : public QMC2_Base { + public: + explicit QMC2_RC4(std::span key); + + void Decrypt(std::span data, size_t offset) override; + + private: + std::vector key_{}; + double hash_{0}; + std::array key_stream_{}; + + size_t DecryptFirstSegment(std::span data, size_t offset); + size_t DecryptOtherSegment(std::span data, size_t offset); +}; + +std::unique_ptr Create(std::string_view ekey); + +} // namespace QMC2 diff --git a/src/qmc2/qmc2_factory.cpp b/src/qmc2/qmc2_factory.cpp new file mode 100644 index 0000000..c4e27ac --- /dev/null +++ b/src/qmc2/qmc2_factory.cpp @@ -0,0 +1,20 @@ +#include "ekey.h" +#include "qmc2.h" + +namespace QMC2 { + +std::unique_ptr Create(std::string_view ekey) { + auto key = decrypt_ekey(ekey); + auto key_len = key.size(); + if (key_len == 0) { + return nullptr; + } + + if (key_len < 300) { + return std::make_unique(std::span(key)); + } else { + return std::make_unique(std::span(key)); + } +} + +} // namespace QMC2 diff --git a/src/qmc2/qmc2_map.cpp b/src/qmc2/qmc2_map.cpp new file mode 100644 index 0000000..d724543 --- /dev/null +++ b/src/qmc2/qmc2_map.cpp @@ -0,0 +1,24 @@ +#include "qmc2.h" + +namespace QMC2 { + +constexpr size_t kMapOffsetBoundary = 0x7FFF; + +QMC2_MAP::QMC2_MAP(std::span key) { + auto n = key.size(); + for (size_t i = 0; i < kMapKeySize; i++) { + size_t j = (i * i + kMapIndexOffset) % n; + const auto shift = (j + 4) % 8; + key_[i] = (key[j] << shift) | (key[j] >> shift); + } +} + +void QMC2_MAP::Decrypt(std::span data, size_t offset) { + for (auto& it : data) { + size_t idx = (offset <= kMapOffsetBoundary) ? offset : (offset % kMapOffsetBoundary); + it ^= key_[idx % key_.size()]; + offset++; + } +} + +} // namespace QMC2 diff --git a/src/qmc2/qmc2_rc4.cpp b/src/qmc2/qmc2_rc4.cpp new file mode 100644 index 0000000..2138003 --- /dev/null +++ b/src/qmc2/qmc2_rc4.cpp @@ -0,0 +1,127 @@ +#include +#include +#include +#include "qmc2.h" + +namespace QMC2 { + +inline double hash(const uint8_t* key, size_t len) { + uint32_t hash = {1}; + const uint8_t* end = key + len; + for (; key < end; ++key) { + if (*key == 0) { + continue; + } + + // Overflow check. + uint32_t next_hash = hash * (uint32_t)(*key); + if (next_hash <= hash) { + break; + } + hash = next_hash; + } + return (double)hash; +} + +class RC4 { + public: + inline RC4(const uint8_t* key, size_t key_len) { + state_.resize(key_len); + for (size_t i = 0; i < key_len; i++) { + state_[i] = static_cast(i); + } + + for (size_t i = 0, j = 0; i < key_len; i++) { + j = (j + state_[i] + key[i]) % key_len; + std::swap(state_[i], state_[j]); + } + + state_len_ = key_len; + } + + inline void Derive(std::span buffer) { + size_t i = i_; + size_t j = j_; + uint8_t* s = state_.data(); + const size_t n = state_len_; + + for (auto& it : buffer) { + i = (i + 1) % n; + j = (j + (size_t)s[i]) % n; + std::swap(s[i], s[j]); + + const size_t final_idx = (s[i] + s[j]) % n; + it ^= s[final_idx]; + } + + i_ = i; + j_ = j; + } + + private: + std::vector state_{}; + size_t state_len_{0}; + size_t i_{0}; + size_t j_{0}; +}; + +inline size_t get_segment_key(double key_hash, size_t segment_id, uint8_t seed) { + if (seed == 0) { + return 0; + } + + double result = key_hash / (double)(seed * (segment_id + 1)) * 100.0; + return (size_t)result; +} + +QMC2_RC4::QMC2_RC4(std::span key) { + hash_ = hash(key.data(), key.size()); + key_ = std::vector(key.begin(), key.end()); + + RC4 rc4(key.data(), key.size()); + rc4.Derive(std::span(key_stream_)); +} + +void QMC2_RC4::Decrypt(std::span data, size_t offset) { + if (offset < kFirstSegmentSize) { + const auto n = DecryptFirstSegment(data, offset); + offset += n; + data = data.subspan(n); + } + + while (!data.empty()) { + const auto n = DecryptOtherSegment(data, offset); + offset += n; + data = data.subspan(n); + } +} + +size_t QMC2_RC4::DecryptFirstSegment(std::span data, size_t offset) { + const uint8_t* key = key_.data(); + const size_t n = this->key_.size(); + + size_t process_len = std::min(data.size(), kFirstSegmentSize - offset); + for (auto& it : data.subspan(0, process_len)) { + const auto idx = get_segment_key(hash_, offset, key[offset % n]) % n; + it ^= key[idx]; + offset++; + } + return process_len; +} + +size_t QMC2_RC4::DecryptOtherSegment(std::span data, size_t offset) { + const size_t n = this->key_.size(); + + size_t segment_idx = offset / kOtherSegmentSize; + size_t segment_offset = offset % kOtherSegmentSize; + + size_t skip_len = get_segment_key(hash_, segment_idx, key_[segment_idx % n]) & 0x1FF; + size_t process_len = std::min(data.size(), kOtherSegmentSize - segment_offset); + const uint8_t* rc4_stream = &key_stream_[skip_len + segment_offset]; + for (auto& it : data.subspan(0, process_len)) { + it ^= *rc4_stream++; + } + return process_len; +} + +} // namespace QMC2 diff --git a/src/tc_tea/tc_tea.cpp b/src/tc_tea/tc_tea.cpp new file mode 100644 index 0000000..1e3748c --- /dev/null +++ b/src/tc_tea/tc_tea.cpp @@ -0,0 +1,70 @@ +#include "tc_tea.h" + +#include "endian_helper.h" +#include "tea_ecb.h" + +#include +#include + +constexpr size_t kTeaBlockSize = 8; +constexpr size_t kFixedSaltLen = 2; +constexpr size_t kZeroPadLen = 7; + +inline void decrypt_round(uint8_t *p_plain, const uint8_t *p_cipher, + uint64_t *iv1, uint64_t *iv2, const uint32_t *key) { + uint64_t iv1_next = Endian::be_u64_read(p_cipher); + uint64_t iv2_next = tc_tea_ecb_decrypt(iv1_next ^ *iv2, key); + uint64_t plain = iv2_next ^ *iv1; + *iv1 = iv1_next; + *iv2 = iv2_next; + Endian::be_u64_write(p_plain, plain); +} + +std::vector tc_tea_cbc_decrypt(std::span cipher, + const uint32_t *key) { + // It needs to have at least 2 blocks long, due to the nature of the padding + // scheme used. + if (cipher.size() % kTeaBlockSize != 0 || cipher.size() < kTeaBlockSize * 2) { + return {}; + } + + uint64_t iv1 = 0; + uint64_t iv2 = 0; + uint8_t header[kTeaBlockSize * 2]; + const uint8_t *in_cipher = cipher.data(); + decrypt_round(header, in_cipher, &iv1, &iv2, key); + in_cipher += kTeaBlockSize; + decrypt_round(header + kTeaBlockSize, in_cipher, &iv1, &iv2, key); + in_cipher += kTeaBlockSize; + + size_t hdr_skip_len = 1 + (header[0] & 7) + kFixedSaltLen; + size_t real_plain_len = cipher.size() - hdr_skip_len - kZeroPadLen; + std::vector result(real_plain_len); + + auto p_output = result.data(); + + // copy first block of plain text + size_t copy_len = std::min(sizeof(header) - hdr_skip_len, real_plain_len); + std::copy_n(header + hdr_skip_len, real_plain_len, p_output); + p_output += copy_len; + + if (real_plain_len != copy_len) { + // Decrypt the rest of the blocks + for (size_t i = cipher.size() - kTeaBlockSize * 3; i != 0; + i -= kTeaBlockSize) { + decrypt_round(p_output, in_cipher, &iv1, &iv2, key); + in_cipher += kTeaBlockSize; + p_output += kTeaBlockSize; + } + + decrypt_round(header + kTeaBlockSize, in_cipher, &iv1, &iv2, key); + p_output[0] = header[kTeaBlockSize]; + } + // Validate zero padding + auto verify = Endian::be_u64_read(header + kTeaBlockSize) << 8; + if (verify != 0) { + result.resize(0); + } + + return result; +} diff --git a/src/tc_tea/tc_tea.h b/src/tc_tea/tc_tea.h new file mode 100644 index 0000000..caee6e7 --- /dev/null +++ b/src/tc_tea/tc_tea.h @@ -0,0 +1,19 @@ +#pragma once + +#include "endian_helper.h" + +#include +#include +#include + +std::vector tc_tea_cbc_decrypt(std::span cipher, + const uint32_t *key); + +inline std::vector tc_tea_cbc_decrypt(std::span cipher, + const uint8_t *key) { + uint32_t key_u32[4]; + for (int i = 0; i < 4; i++) { + key_u32[i] = Endian::be_u32_read(key + i * 4); + } + return tc_tea_cbc_decrypt(cipher, key_u32); +} diff --git a/src/tc_tea/tea_ecb.h b/src/tc_tea/tea_ecb.h new file mode 100644 index 0000000..e5baec2 --- /dev/null +++ b/src/tc_tea/tea_ecb.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +constexpr size_t TEA_ROUNDS = (16); +constexpr uint32_t TEA_ROUND_DELTA = (0x9e3779b9); +constexpr uint32_t TEA_EXPECTED_SUM = (static_cast(TEA_ROUNDS * TEA_ROUND_DELTA)); + +inline uint32_t tc_tea_single_round(uint32_t value, uint32_t sum, uint32_t key1, uint32_t key2) { + return ((value << 4) + key1) ^ (value + sum) ^ ((value >> 5) + key2); +} + +inline uint64_t tc_tea_ecb_decrypt(uint64_t value, const uint32_t* key) { + uint32_t y = (uint32_t)(value >> 32); + uint32_t z = (uint32_t)(value); + uint32_t sum = {TEA_EXPECTED_SUM}; + + for (size_t i = 0; i < TEA_ROUNDS; i++) { + z -= tc_tea_single_round(y, sum, key[2], key[3]); + y -= tc_tea_single_round(z, sum, key[0], key[1]); + sum -= TEA_ROUND_DELTA; + } + + return ((uint64_t)(y) << 32) | (uint64_t)(z); +} + +inline uint64_t tc_tea_ecb_encrypt(uint64_t value, const uint32_t* key) { + uint32_t y = (uint32_t)(value >> 32); + uint32_t z = (uint32_t)(value); + uint32_t sum = {0}; + + for (size_t i = 0; i < TEA_ROUNDS; i++) { + sum += TEA_ROUND_DELTA; + y += tc_tea_single_round(z, sum, key[0], key[1]); + z += tc_tea_single_round(y, sum, key[2], key[3]); + } + + return ((uint64_t)(y) << 32) | (uint64_t)(z); +}