// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Pure (no-I/O) core of the xmrig updater: release-JSON parsing, platform/asset matching, checksum // parsing, and SHA-256 hashing. Split from xmrig_updater.cpp (the libcurl/miniz worker) so it can // be unit-tested in the test binary, which links libsodium + nlohmann/json but not curl/miniz. #include "xmrig_updater.h" #include #include #include #include #include #include using json = nlohmann::json; namespace dragonx { namespace util { namespace { std::string toLower(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return s; } bool isHex64(const std::string& s) { if (s.size() != 64) return false; for (unsigned char c : s) if (!std::isxdigit(c)) return false; return true; } } // namespace namespace { // Fill an XmrigRelease from one Gitea release JSON object (ok=true iff it has a tag). XmrigRelease parseOneXmrigRelease(const json& j) { XmrigRelease r; if (!j.is_object()) return r; if (j.contains("tag_name") && j["tag_name"].is_string()) r.tag = j["tag_name"].get(); if (j.contains("name") && j["name"].is_string()) r.name = j["name"].get(); if (j.contains("body") && j["body"].is_string()) r.body = j["body"].get(); if (j.contains("prerelease") && j["prerelease"].is_boolean()) r.prerelease = j["prerelease"].get(); if (j.contains("published_at") && j["published_at"].is_string()) r.publishedAt = j["published_at"].get(); if (j.contains("assets") && j["assets"].is_array()) { for (const auto& a : j["assets"]) { if (!a.is_object()) continue; XmrigReleaseAsset asset; if (a.contains("name") && a["name"].is_string()) asset.name = a["name"].get(); if (a.contains("browser_download_url") && a["browser_download_url"].is_string()) asset.downloadUrl = a["browser_download_url"].get(); if (a.contains("size") && a["size"].is_number_integer()) asset.size = a["size"].get(); if (!asset.name.empty() && !asset.downloadUrl.empty()) r.assets.push_back(std::move(asset)); } } if (!r.tag.empty()) r.ok = true; return r; } } // namespace XmrigRelease parseXmrigRelease(const std::string& jsonStr) { XmrigRelease r; try { r = parseOneXmrigRelease(json::parse(jsonStr)); if (!r.ok) r.error = "release JSON has no tag_name"; } catch (const std::exception& e) { r.error = std::string("failed to parse release JSON: ") + e.what(); } return r; } std::vector parseXmrigReleaseList(const std::string& jsonStr) { std::vector out; try { const json j = json::parse(jsonStr); if (!j.is_array()) return out; for (const auto& e : j) { if (e.contains("draft") && e["draft"].is_boolean() && e["draft"].get()) continue; // skip drafts (not meant for end users) XmrigRelease r = parseOneXmrigRelease(e); if (r.ok) out.push_back(std::move(r)); } } catch (const std::exception&) { // malformed list -> empty (caller treats as "could not load releases") } return out; } std::string currentXmrigPlatformToken() { #if defined(_WIN32) return "win-x64"; #elif defined(__APPLE__) #if defined(__aarch64__) || defined(__arm64__) return "macos-arm64"; // no arm64 build published yet -> resolves to Unavailable #else return "macos-x86_64"; // matches the release asset naming (note: not "macos-x64") #endif #elif defined(__linux__) #if defined(__aarch64__) return "linux-arm64"; #else return "linux-x64"; #endif #else return ""; #endif } int selectXmrigAsset(const XmrigRelease& release, const std::string& platformToken) { if (platformToken.empty()) return -1; const std::string needle = "-" + toLower(platformToken) + ".zip"; for (std::size_t i = 0; i < release.assets.size(); ++i) { const std::string n = toLower(release.assets[i].name); if (n.size() >= needle.size() && n.compare(n.size() - needle.size(), needle.size(), needle) == 0) return static_cast(i); } return -1; } std::map parseXmrigChecksums(const std::string& body) { std::map out; std::istringstream in(body); std::string line; while (std::getline(in, line)) { std::istringstream ls(line); std::string hash, name; if (!(ls >> hash)) continue; if (!isHex64(hash)) continue; if (!(ls >> name)) continue; // First whitespace-delimited token after the hash is the key (zip name, or "xmrig"/ // "xmrig.exe" for the inner-binary block; any trailing "(linux-x64)" annotation is ignored). out[name] = toLower(hash); } return out; } std::string sha256Hex(const void* data, std::size_t len) { unsigned char hash[crypto_hash_sha256_BYTES]; if (crypto_hash_sha256(hash, static_cast(data), len) != 0) return {}; static const char* kHex = "0123456789abcdef"; std::string out; out.reserve(crypto_hash_sha256_BYTES * 2); for (unsigned char c : hash) { out.push_back(kHex[c >> 4]); out.push_back(kHex[c & 0x0F]); } return out; } std::vector xmrigExtractBasenames(const std::string& platformToken) { if (platformToken.rfind("win", 0) == 0) return {"xmrig.exe", "WinRing0x64.sys"}; return {"xmrig"}; } int selectXmrigSignatureAsset(const XmrigRelease& release, const std::string& archiveName) { if (archiveName.empty()) return -1; for (std::size_t i = 0; i < release.assets.size(); ++i) { const std::string& n = release.assets[i].name; if (n == archiveName + ".sig" || n == archiveName + ".minisig") return static_cast(i); } return -1; } namespace { // Decode base64 (whitespace tolerated) into exactly `expected` bytes; false on any mismatch. bool base64ToFixed(const std::string& b64, unsigned char* out, std::size_t expected) { std::size_t binLen = 0; const char* end = nullptr; if (sodium_base642bin(out, expected, b64.data(), b64.size(), " \t\r\n", &binLen, &end, sodium_base64_VARIANT_ORIGINAL) != 0) return false; return binLen == expected; } } // namespace bool verifyXmrigSignature(const std::string& data, const std::string& signatureFileContent, const std::string& pubKeyBase64) { if (sodium_init() < 0) return false; unsigned char pubKey[crypto_sign_PUBLICKEYBYTES]; if (!base64ToFixed(pubKeyBase64, pubKey, sizeof(pubKey))) return false; // Accept either base64 (what the .sig assets contain) or a raw 64-byte signature. Try base64 // first on whitespace-trimmed text; only if that fails fall back to RAW bytes, using the EXACT // untrimmed content — so a raw signature byte that happens to equal a whitespace value (~1.6% of // random signatures end in one) is never stripped/corrupted. A raw 64-byte input can't be // mistaken for base64: 64 base64 chars decode to 48 bytes, never 64, so base64ToFixed rejects it. std::string trimmed = signatureFileContent; while (!trimmed.empty() && (trimmed.back() == '\n' || trimmed.back() == '\r' || trimmed.back() == ' ' || trimmed.back() == '\t')) trimmed.pop_back(); unsigned char sig[crypto_sign_BYTES]; if (!base64ToFixed(trimmed, sig, sizeof(sig))) { if (signatureFileContent.size() == crypto_sign_BYTES) std::memcpy(sig, signatureFileContent.data(), crypto_sign_BYTES); else return false; } return crypto_sign_verify_detached( sig, reinterpret_cast(data.data()), data.size(), pubKey) == 0; } } // namespace util } // namespace dragonx