diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c60aee..99a2b0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(XiaomiOTA +project(MiTool-CLI LANGUAGES CXX VERSION 0.0.1 ) @@ -15,5 +15,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) find_package(OpenSSL REQUIRED) find_package(nlohmann_json REQUIRED) find_package(cpr REQUIRED) +find_package(fmt REQUIRED) +find_package(CLI11 REQUIRED) add_subdirectory(src) diff --git a/conanfile.txt b/conanfile.txt index cc7310d..455dbd4 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -2,6 +2,8 @@ openssl/3.6.2 nlohmann_json/3.12.0 cpr/1.14.2 +fmt/12.1.0 +cli11/2.6.2 [generators] CMakeDeps CMakeToolchain diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1dc7d7b..7e7673a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,9 +1,22 @@ -add_executable(xmota - main.cc - ) +add_library(MiuiUpdate + MiuiUpdate/MiuiCrypto.cc + MiuiUpdate/MiuiFindUpdate.cc + MiuiUpdate/MiuiParseVersion.cc +) + +add_executable(mitool + Main.cc +) -target_link_libraries(xmota +target_link_libraries(MiuiUpdate PRIVATE openssl::openssl PRIVATE cpr::cpr PRIVATE nlohmann_json::nlohmann_json - ) \ No newline at end of file + ) + +target_link_libraries(mitool + PRIVATE nlohmann_json::nlohmann_json + PRIVATE fmt::fmt + PRIVATE CLI11::CLI11 + PRIVATE MiuiUpdate + ) diff --git a/src/Main.cc b/src/Main.cc new file mode 100644 index 0000000..0f596ae --- /dev/null +++ b/src/Main.cc @@ -0,0 +1,141 @@ +#include "MiuiUpdate/MiuiUpdate.h" + +#include +#include +#include + +#include +#include +#include +#include + +using json = nlohmann::json; + +static constexpr const char *ASCII_ART = + "__ __ ___ _____ ___ ___ _ ____ _ ___ \n" + "| \\/ |_ _|_ _/ _ \\ / _ \\| | / ___| | |_ _|\n" + "| |\\/| || | | || | | | | | | | _____| | | | | | \n" + "| | | || | | || |_| | |_| | |__|_____| |___| |___ | | \n" + "|_| |_|___| |_| \\___/ \\___/|_____| \\____|_____|___|\n"; + +struct LatestUpdateInfo { + std::string device; + std::string currentRom; + std::string latestRom; + std::string androidVer; + std::string fileSize; + std::string applicableFrom; + std::string md5; + std::vector changelog; +}; + +static std::string safeGet(const json &obj, const std::string &key) { + if (!obj.is_null() && obj.contains(key) && obj.at(key).is_string()) { + return obj.at(key).get(); + } + return "N/A"; +} + +static LatestUpdateInfo parseUpdateInfo(const std::string &jsonStr) { + const json data = json::parse(jsonStr); + + const json &latestRom = data["LatestRom"]; + const json ¤tRom = data["CurrentRom"]; + const json &incrementRom = data["IncrementRom"]; + + std::vector changelog; + if (latestRom.contains("changelog")) { + const json &cl = latestRom.at("changelog"); + if (cl.contains("System") && cl.at("System").contains("txt")) { + const json &txt = cl.at("System").at("txt"); + if (txt.is_array()) { + for (const auto &entry : txt) { + if (entry.is_string()) { + changelog.push_back(entry.get()); + } + } + } + } + } + + if (changelog.empty()) { + changelog.push_back("N/A"); + } + + return LatestUpdateInfo{ + .device = safeGet(latestRom, "device"), + .currentRom = safeGet(currentRom, "version"), + .latestRom = safeGet(latestRom, "version"), + .androidVer = safeGet(latestRom, "codebase"), + .fileSize = safeGet(latestRom, "filesize"), + .applicableFrom = safeGet(incrementRom, "versionForApply"), + .md5 = safeGet(latestRom, "md5"), + .changelog = changelog, + }; +} + +static void printUpdateInfo(const LatestUpdateInfo &info) { + static constexpr const char *SEP = + "├─────────────────────────────────────────────────────────────────"; + + fmt::println( + "┌─────────────────────────────────────────────────────────────────"); + fmt::println("│ MIUI OTA — Update Information"); + fmt::println("{}", SEP); + fmt::println("│ Device : {}", info.device); + fmt::println("│ Current ROM : {}", info.currentRom); + fmt::println("│ Latest ROM : {}", info.latestRom); + fmt::println("│ Android version : {}", info.androidVer); + fmt::println("│ File size : {}", info.fileSize); + fmt::println("│ Applicable from : {}", info.applicableFrom); + fmt::println("│ MD5 : {}", info.md5); + fmt::println("{}", SEP); + fmt::println("│ Changelog:"); + for (std::size_t i = 0; i < info.changelog.size(); ++i) { + fmt::println("│ {}. {}", i + 1, info.changelog[i]); + } + fmt::println( + "└─────────────────────────────────────────────────────────────────"); +} + +int main(int argc, char *argv[]) { + std::string osVersion; + std::string deviceCodename; + bool noBannerFlag{false}; + + CLI::App app{"A simple utility for finding, downloading, and installing " + "firmware updates"}; + + app.add_option("--os-version", osVersion, + "Specify the OS version currently installed on the device"); + app.add_option("--device", deviceCodename, "Specify the device codename"); + app.add_flag("--no-banner", noBannerFlag, "Disable ASCII startup banner"); + + CLI11_PARSE(app, argc, argv); + + if (!noBannerFlag) { + fmt::println("{}", ASCII_ART); + } + + std::transform(osVersion.begin(), osVersion.end(), osVersion.begin(), ::toupper); + std::transform(deviceCodename.begin(), deviceCodename.end(), deviceCodename.begin(), ::tolower); + + MiuiUpdater updater; + const std::string jsonResponse = + updater.getLatestUpdate(deviceCodename, osVersion); + + LatestUpdateInfo info; + try { + info = parseUpdateInfo(jsonResponse); + } catch (const json::parse_error &e) { + fmt::println("[ERROR] Failed to parse server response: {}", e.what()); + return 1; + } catch (const json::out_of_range &e) { + fmt::println("[ERROR] Unexpected response structure: {}", e.what()); + return 1; + } + + printUpdateInfo(info); + + return 0; +} diff --git a/src/MiuiOTA.h b/src/MiuiOTA.h deleted file mode 100644 index 980636e..0000000 --- a/src/MiuiOTA.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - -struct MiuiVersion { - char os_ver[3]; - uint16_t major; - uint16_t minor; - uint16_t patch; - char android_letter; - char device_letter[3]; - char region_letter[3]; - char carier[3]; -}; - -struct DeviceData { - MiuiVersion version; - std::string region_full; - double codebase; -}; \ No newline at end of file diff --git a/src/MiuiUpdate/MiuiCrypto.cc b/src/MiuiUpdate/MiuiCrypto.cc new file mode 100644 index 0000000..0a2dff7 --- /dev/null +++ b/src/MiuiUpdate/MiuiCrypto.cc @@ -0,0 +1,121 @@ +#include "MiuiKeys.h" +#include "MiuiUpdate.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +std::vector MiuiUpdater::base64Decrypt(const std::string &text) { + BIO *bmem = BIO_new_mem_buf(text.data(), static_cast(text.size())); + BIO *b64 = BIO_new(BIO_f_base64()); + + if (!bmem || !b64) + throw std::runtime_error("base64Decrypt: failed to create BIO"); + + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + BIO_push(b64, bmem); + + std::vector buffer(text.size()); + int totalLen = 0; + + while (true) { + int len = BIO_read(b64, buffer.data() + totalLen, + static_cast(buffer.size()) - totalLen); + if (len <= 0) + break; + totalLen += len; + } + + BIO_free_all(b64); + buffer.resize(totalLen); + return buffer; +} + +std::string MiuiUpdater::base64Encrypt(std::vector &data) { + BIO *b64 = BIO_new(BIO_f_base64()); + BIO *bmem = BIO_new(BIO_s_mem()); + + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + BIO_push(b64, bmem); + + BIO_write(b64, data.data(), static_cast(data.size())); + BIO_flush(b64); + + BUF_MEM *bptr = nullptr; + BIO_get_mem_ptr(bmem, &bptr); + + std::string result(bptr->data, bptr->length); + BIO_free_all(b64); + + return result; +} + +std::vector +MiuiUpdater::aes128cbcEncrypt(const std::string &data) { + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + throw std::runtime_error("aes128cbcEncrypt: failed to create context"); + + if (EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), nullptr, MIUI_CRYPTO_KEY, + MIUI_CRYPTO_IV) != 1) { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("aes128cbcEncrypt: initialization error"); + } + + std::vector ciphertext(data.size() + AES_BLOCK_SIZE); + int outLen1 = 0, outLen2 = 0; + + if (EVP_EncryptUpdate(ctx, ciphertext.data(), &outLen1, + reinterpret_cast(data.data()), + static_cast(data.size())) != 1) { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("aes128cbcEncrypt: encryption error (Update)"); + } + + if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + outLen1, &outLen2) != 1) { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("aes128cbcEncrypt: encryption error (Final)"); + } + + EVP_CIPHER_CTX_free(ctx); + ciphertext.resize(outLen1 + outLen2); + return ciphertext; +} + +std::string MiuiUpdater::aes128cbcDecrypt(std::vector &data) { + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (!ctx) + throw std::runtime_error("aes128cbcDecrypt: failed to create context"); + + if (EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), nullptr, MIUI_CRYPTO_KEY, + MIUI_CRYPTO_IV) != 1) { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("aes128cbcDecrypt: initialization error"); + } + + std::vector plaintext(data.size() + AES_BLOCK_SIZE); + int outLen1 = 0, outLen2 = 0; + + if (EVP_DecryptUpdate(ctx, plaintext.data(), &outLen1, data.data(), + static_cast(data.size())) != 1) { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error("aes128cbcDecrypt: decryption error (Update)"); + } + + if (EVP_DecryptFinal_ex(ctx, plaintext.data() + outLen1, &outLen2) != 1) { + EVP_CIPHER_CTX_free(ctx); + throw std::runtime_error( + "aes128cbcDecrypt: decryption error (Final) — invalid key/IV?"); + } + + EVP_CIPHER_CTX_free(ctx); + return std::string(plaintext.begin(), plaintext.begin() + outLen1 + outLen2); +} diff --git a/src/MiuiUpdate/MiuiFindUpdate.cc b/src/MiuiUpdate/MiuiFindUpdate.cc new file mode 100644 index 0000000..df8d0be --- /dev/null +++ b/src/MiuiUpdate/MiuiFindUpdate.cc @@ -0,0 +1,100 @@ +#include "MiuiKeys.h" +#include "MiuiUpdate.h" + +#include +#include +#include + +#include +#include + +using json = nlohmann::json; + +json requestJson = { + {"a", 0}, + {"c", ""}, + {"b", "F"}, + {"d", ""}, + {"g", "00000000000000000000000000000000"}, + {"i", "0000000000000000000000000000000000000000000000000000000000000000"}, + {"isR", "0"}, + {"f", "1"}, + {"l", "en_US"}, + {"n", ""}, + {"sys", "0"}, + {"unlock", "0"}, + {"r", "CN"}, + {"sn", "0x00000000"}, + {"v", ""}, + {"bv", ""}, + {"id", ""}}; + +static std::string buildOsVersion(const DeviceData &data) { + const std::string versionBlock = + std::string(data.version.osVer) + std::to_string(data.version.major); + + std::string fullVersion; + if (data.version.osVer[0] == 'V') { + fullVersion = "MIUI-"; + } + + fullVersion += + std::vformat("{}.{}.{}.{}.{}{}{}{}", + std::make_format_args( + versionBlock, data.version.minor, data.version.patch, + data.version.build, data.version.androidLetter, + data.version.deviceLetter, data.version.regionLetter, + data.version.carier)); + + return fullVersion; +} + +std::string MiuiUpdater::requestForUpdate(const std::string &requestData) { + auto encrypted = aes128cbcEncrypt(requestJson.dump()); + const auto encoded = base64Encrypt(encrypted); + + // URL encode + const auto escaped = cpr::util::urlEncode(encoded); + + // POST body + const std::string postBody = "q=" + (std::string)escaped + "&t=&s=1"; + + // Request + auto response = cpr::Post( + cpr::Url{MIUI_OTA_URL}, + + cpr::Header{{"clientId", "MITUNES"}, + {"Connection", "Keep-Alive"}, + {"Accept-Encoding", "identity"}, + {"Content-Type", "application/x-www-form-urlencoded"}}, + + cpr::UserAgent{"MiTunes_UserAgent_v3.0"}, + + cpr::Body{postBody}); + + // Network error + if (response.error.code != cpr::ErrorCode::OK) { + return "N/A"; + } + + // Response decode + const auto decodedResponse = cpr::util::urlDecode(response.text); + + auto decryptedBase64 = base64Decrypt((std::string)decodedResponse); + + return aes128cbcDecrypt(decryptedBase64); +} + +std::string MiuiUpdater::getLatestUpdate(const std::string &device, + const std::string &version) { + + if (!parseVersion(version)) + return ""; + + requestJson["d"] = device + data.regionFull; + requestJson["c"] = data.codebase; + requestJson["bv"] = data.bv; + requestJson["v"] = buildOsVersion(data); + + return requestForUpdate(requestJson.dump()); +} diff --git a/src/MiuiUpdate/MiuiKeys.h b/src/MiuiUpdate/MiuiKeys.h new file mode 100644 index 0000000..cf45325 --- /dev/null +++ b/src/MiuiUpdate/MiuiKeys.h @@ -0,0 +1,10 @@ +#pragma once + +#define MIUI_OTA_URL "http://update.miui.com/updates/miotaV3.php" +#define MIUI_FASTBOOT_URL +const unsigned char MIUI_CRYPTO_KEY[16] = {0x6D, 0x69, 0x75, 0x69, 0x6F, 0x74, + 0x61, 0x76, 0x61, 0x6C, 0x69, 0x64, + 0x65, 0x64, 0x31, 0x31}; +const unsigned char MIUI_CRYPTO_IV[16] = {0x30, 0x31, 0x30, 0x32, 0x30, 0x33, + 0x30, 0x34, 0x30, 0x35, 0x30, 0x36, + 0x30, 0x37, 0x30, 0x38}; diff --git a/src/MiuiUpdate/MiuiParseVersion.cc b/src/MiuiUpdate/MiuiParseVersion.cc new file mode 100644 index 0000000..600282a --- /dev/null +++ b/src/MiuiUpdate/MiuiParseVersion.cc @@ -0,0 +1,105 @@ +#include "MiuiUpdate.h" + +#include +#include +#include +#include +#include + +static double detectAndroidVersion(const char letter) { + switch (letter) { + case 'W': + return 16.0; + case 'V': + return 15.0; + case 'U': + return 14.0; + case 'T': + return 13.0; + case 'S': + return 12.0; + case 'R': + return 11.0; + case 'Q': + return 10.0; + case 'P': + return 9.0; + case 'O': + return 8.0; + case 'N': + return 7.0; + case 'M': + return 6.0; + default: + return 0.0; + } +} + +static std::string detectRegion(const char region[3]) { + static const std::unordered_map regionMap = { + {"MI", "_global"}, {"EU", "_eea_global"}, {"RU", "_ru_global"}, + {"TW", "_tw_global"}, {"ID", "_id_global"}, {"IN", "_in_global"}, + {"CN", ""}, + }; + + auto it = regionMap.find(std::string(region, 2)); + return it != regionMap.end() ? it->second : ""; +} + +static std::string detectBv(const char osVer[4], std::uint16_t major) { + if (osVer[0] != 'V') { + return ""; + } + if (major <= 13) { + return std::to_string(major); + } + return ""; +} + +bool MiuiUpdater::parseVersion(const std::string &input) { + static const std::regex re( + R"(^([A-Z]+)(\d+)\.(\d+)\.(\d+)\.(\d+)\.([A-Z])([A-Z])([A-Z])([A-Z]{2})(.*)$)"); + + std::smatch match; + if (!std::regex_match(input, match, re)) { + return false; + } + + std::strncpy(data.version.osVer, match[1].str().c_str(), + sizeof(data.version.osVer) - 1); + data.version.osVer[sizeof(data.version.osVer) - 1] = '\0'; + + const uint16_t rawMajor = static_cast(std::stoi(match[2].str())); + data.version.major = rawMajor; + data.version.minor = static_cast(std::stoi(match[3].str())); + data.version.patch = static_cast(std::stoi(match[4].str())); + data.version.build = static_cast(std::stoi(match[5].str())); + + data.version.androidLetter = match[6].str()[0]; + + // deviceLetter = match[7] + match[8] → "CD" + const std::string deviceStr = match[7].str() + match[8].str(); + std::strncpy(data.version.deviceLetter, deviceStr.c_str(), + sizeof(data.version.deviceLetter) - 1); + data.version.deviceLetter[sizeof(data.version.deviceLetter) - 1] = '\0'; + + // regionLetter = match[9] → "MI" + std::strncpy(data.version.regionLetter, match[9].str().c_str(), + sizeof(data.version.regionLetter) - 1); + data.version.regionLetter[sizeof(data.version.regionLetter) - 1] = '\0'; + + // carrier = match[10] → "XM" + std::strncpy(data.version.carier, match[10].str().c_str(), + sizeof(data.version.carier) - 1); + data.version.carier[sizeof(data.version.carier) - 1] = '\0'; + + // DeviceData fields + data.codebase = detectAndroidVersion(data.version.androidLetter); + data.regionFull = detectRegion(data.version.regionLetter); + + const std::string bv = detectBv(data.version.osVer, rawMajor); + std::strncpy(data.bv, bv.c_str(), sizeof(data.bv) - 1); + data.bv[sizeof(data.bv) - 1] = '\0'; + + return true; +} diff --git a/src/MiuiUpdate/MiuiUpdate.h b/src/MiuiUpdate/MiuiUpdate.h new file mode 100644 index 0000000..f7acf9c --- /dev/null +++ b/src/MiuiUpdate/MiuiUpdate.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +struct MiuiVersion { + char osVer[4]; + std::uint16_t major; + std::uint16_t minor; + std::uint16_t patch; + std::uint16_t build; + char androidLetter; + char deviceLetter[3]; + char regionLetter[3]; + char carier[3]; +}; + +struct DeviceData { + MiuiVersion version; + + std::string regionFull; + std::string device; + + double codebase; + + char bv[8]; +}; + +class MiuiUpdater { +private: + DeviceData data; + +private: + bool parseVersion(const std::string &version); + + std::vector base64Decrypt(const std::string &data); + std::string base64Encrypt(std::vector &data); + + std::vector aes128cbcEncrypt(const std::string &data); + std::string aes128cbcDecrypt(std::vector &data); + + std::string requestForUpdate(const std::string &requestData); + +public: + std::string getLatestUpdate(const std::string &device, const std::string &version); +}; diff --git a/src/main.cc b/src/main.cc deleted file mode 100644 index f700770..0000000 --- a/src/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include - -int main(int argc, char *argv[]) { - std::cout << "Hello, World!\n"; - return 0; -} \ No newline at end of file