// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "connection.h" #include "../config/version.h" #include "../resources/embedded_resources.h" #include #include #include #include #include #include #include #include #include #include "../util/logger.h" #include "../util/platform.h" #include "../util/xmrig_updater.h" // util::sha256Hex #ifdef _WIN32 #include #else #include #include #endif namespace fs = std::filesystem; namespace dragonx { namespace rpc { namespace { std::string generateSecureRandomString(size_t length) { static constexpr char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; static constexpr uint32_t charsetSize = static_cast(sizeof(charset) - 1); if (sodium_init() < 0) { DEBUG_LOGF("Failed to initialize libsodium for RPC credential generation\n"); return {}; } std::string result; result.reserve(length); for (size_t i = 0; i < length; ++i) { result.push_back(charset[randombytes_uniform(charsetSize)]); } return result; } std::string lowercase(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return value; } bool parseBoolValue(const std::string& value) { std::string lowered = lowercase(value); return lowered == "1" || lowered == "true" || lowered == "yes" || lowered == "on"; } bool applyCookieAuth(ConnectionConfig& config, const std::string& dataDir) { std::string cookieUser, cookiePass; if (!Connection::readAuthCookie(dataDir, cookieUser, cookiePass)) { return false; } config.rpcuser = cookieUser; config.rpcpassword = cookiePass; config.auth_source = AuthSource::Cookie; if (config.hush_dir.empty()) config.hush_dir = dataDir; return true; } } // namespace Connection::Connection() = default; Connection::~Connection() = default; std::string Connection::getDefaultDataDir() { #ifdef _WIN32 char path[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) { return std::string(path) + "\\Hush\\DRAGONX"; } return ""; #elif defined(__APPLE__) const char* home = getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); home = pw->pw_dir; } // Match SilentDragonX path: Library/Application Support/Hush/DRAGONX return std::string(home) + "/Library/Application Support/Hush/DRAGONX"; #else const char* home = getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); home = pw->pw_dir; } return std::string(home) + "/.hush/DRAGONX"; #endif } std::string Connection::getDefaultConfPath() { return getDefaultDataDir() + "/" + DRAGONX_CONF_FILENAME; } std::string Connection::getSaplingParamsDir() { // Sapling params are now extracted alongside the daemon binaries // in /dragonx/ — no longer in the legacy ZcashParams dir. return resources::getDaemonDirectory(); } namespace { std::string joinParamPath(const std::string& dir, const std::string& file) { #ifdef _WIN32 return dir + "\\" + file; #else return dir + "/" + file; #endif } // ":" fingerprint used to skip re-hashing an unchanged file. Empty on error. std::string paramStatLine(const std::string& path) { std::error_code ec; auto sz = fs::file_size(path, ec); if (ec) return {}; auto mtime = fs::last_write_time(path, ec); long long ticks = ec ? 0 : std::chrono::duration_cast(mtime.time_since_epoch()).count(); return std::to_string(static_cast(sz)) + ":" + std::to_string(ticks); } bool paramHashMatches(const std::string& path, const std::string& expectedHex) { std::ifstream f(path, std::ios::binary | std::ios::ate); if (!f) return false; std::streamsize sz = f.tellg(); if (sz <= 0) return false; f.seekg(0, std::ios::beg); std::vector buf(static_cast(sz)); if (!f.read(buf.data(), sz)) return false; std::string got = util::sha256Hex(buf.data(), buf.size()); return !got.empty() && got == expectedHex; } // The verification cache: /.sapling_verified holds one paramStatLine per param, // in list order, from the last successful hash check. bool saplingMarkerMatches(const std::string& markerPath, const std::vector& expected) { for (const auto& s : expected) if (s.empty()) return false; // couldn't stat -> don't trust std::ifstream f(markerPath); if (!f) return false; std::vector lines; std::string l; while (std::getline(f, l)) lines.push_back(l); return lines == expected; } void writeSaplingMarker(const std::string& markerPath, const std::vector& lines) { std::ofstream f(markerPath, std::ios::trunc); if (!f) return; for (const auto& l : lines) f << l << "\n"; } // Canonical Zcash-family Sapling trusted-setup param digests — identical bytes across every // fork/platform. Source of truth: scripts/build-lite-backend-artifact.sh ensure_sapling_params(). // Keep in sync if the params are ever rotated. const std::pair kSaplingParamDigests[] = { { "sapling-spend.params", "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13" }, { "sapling-output.params", "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4" }, }; } // namespace bool Connection::verifySaplingParamsIn( const std::string& dir, const std::vector>& digests) { if (dir.empty()) { DEBUG_LOGF("verifySaplingParams: params dir is empty\n"); return false; } if (digests.empty()) return false; // 1) Every param must exist. std::vector paths; paths.reserve(digests.size()); for (const auto& d : digests) { std::string p = joinParamPath(dir, d.first); if (!fs::exists(p)) { DEBUG_LOGF("verifySaplingParams: %s MISSING\n", p.c_str()); return false; } paths.push_back(std::move(p)); } // 2) Fast path: if the cached marker matches the current size:mtime of every param, trust // the previous successful hash instead of re-hashing ~48MB on every startup. const std::string markerPath = joinParamPath(dir, ".sapling_verified"); std::vector current; current.reserve(paths.size()); for (const auto& p : paths) current.push_back(paramStatLine(p)); if (saplingMarkerMatches(markerPath, current)) { return true; } // 3) Integrity-check each param against its pinned SHA-256. A truncated or corrupt param // (a partial extraction, or a Linux bundle where the file merely *exists*) is rejected // here instead of being handed to the daemon and failing later on a shielded operation. for (size_t i = 0; i < paths.size(); ++i) { if (!paramHashMatches(paths[i], digests[i].second)) { DEBUG_LOGF("verifySaplingParams: %s FAILED integrity check (truncated or corrupt)\n", paths[i].c_str()); return false; } } // 4) Record the verified state so later startups take the fast path. writeSaplingMarker(markerPath, current); DEBUG_LOGF("verifySaplingParams: %zu params verified (sha256)\n", paths.size()); return true; } bool Connection::verifySaplingParams() { std::vector> digests; for (const auto& d : kSaplingParamDigests) digests.emplace_back(d.first, d.second); return verifySaplingParamsIn(getSaplingParamsDir(), digests); } ConnectionConfig Connection::parseConfFile(const std::string& path) { ConnectionConfig config; std::ifstream file(path); if (!file.is_open()) { return config; } std::string line; while (std::getline(file, line)) { // Skip empty lines and comments if (line.empty() || line[0] == '#') { continue; } // Parse key=value size_t eq_pos = line.find('='); if (eq_pos == std::string::npos) { continue; } std::string key = line.substr(0, eq_pos); std::string value = line.substr(eq_pos + 1); // Trim whitespace (including \r from Windows line endings) while (!key.empty() && (key.back() == ' ' || key.back() == '\t' || key.back() == '\r')) { key.pop_back(); } while (!value.empty() && (value[0] == ' ' || value[0] == '\t')) { value.erase(0, 1); } while (!value.empty() && (value.back() == ' ' || value.back() == '\t' || value.back() == '\r')) { value.pop_back(); } // Map to config if (key == "rpcuser") { config.rpcuser = value; } else if (key == "rpcpassword") { config.rpcpassword = value; } else if (key == "rpcport") { config.port = value; } else if (key == "rpchost" || key == "rpcconnect") { config.host = value; } else if (key == "proxy") { config.proxy = value; } else if (key == "rpctls" || key == "rpcssl" || key == "use_tls" || key == "rpcuse_tls") { config.use_tls = parseBoolValue(value); } else if (key == "rpcallowplaintext") { config.allow_plaintext_remote = parseBoolValue(value); } } if (!config.rpcuser.empty() || !config.rpcpassword.empty()) { config.auth_source = AuthSource::ConfigFile; } return config; } ConnectionConfig Connection::autoDetectConfig() { ConnectionConfig config; // Ensure the data directory exists. Use the non-throwing helper and report any failure // via config.dir_error so callers can surface it — the old throwing create_directories() // overload could raise an uncaught filesystem_error straight through autoDetectConfig()'s // callers (read-only home, permission denied, etc.). std::string data_dir = getDefaultDataDir(); if (!util::Platform::ensureDirectory(data_dir, &config.dir_error)) { DEBUG_LOGF("[ERROR] autoDetectConfig: %s\n", config.dir_error.c_str()); return config; // data dir unusable — bail early with dir_error set } // Try to find DRAGONX.conf std::string conf_path = getDefaultConfPath(); if (fs::exists(conf_path)) { // Ensure exportdir is present in existing config ensureExportDir(conf_path); // Ensure encryption flags are present ensureEncryptionEnabled(conf_path); config = parseConfFile(conf_path); config.hush_dir = data_dir; } else { // Create a default config file if (createDefaultConfig(conf_path)) { config = parseConfFile(conf_path); config.hush_dir = data_dir; } } // If rpcpassword is empty, the daemon may be using .cookie auth if (config.rpcpassword.empty()) { if (applyCookieAuth(config, data_dir)) { DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n"); } } // Set defaults for missing values if (config.host.empty()) { config.host = DRAGONX_DEFAULT_RPC_HOST; } if (config.port.empty()) { config.port = DRAGONX_DEFAULT_RPC_PORT; } return config; } bool Connection::buildCookieAuthConfig(const ConnectionConfig& base, ConnectionConfig& cookieConfig) { if (base.auth_source == AuthSource::Cookie) { return false; } std::string dataDir = base.hush_dir.empty() ? getDefaultDataDir() : base.hush_dir; ConnectionConfig fallback = base; if (!applyCookieAuth(fallback, dataDir)) { return false; } cookieConfig = std::move(fallback); return true; } // True only for a well-formed IPv4 loopback literal (127.0.0.0/8): exactly four dot-separated // 0-255 octets with the first == 127. Rejects "127.evil.com", "127.0.0.1.attacker", // "127.300.0.1", "1270.0.0.1", etc. — the old rfind("127.",0)==0 prefix matched all of those. static bool isExactIPv4Loopback(const std::string& host) { int octets = 0, value = 0, digits = 0; bool firstIs127 = false; for (size_t i = 0; i <= host.size(); ++i) { const char c = (i < host.size()) ? host[i] : '.'; // trailing sentinel flushes the last octet if (c == '.') { if (digits == 0 || digits > 3 || value > 255) return false; if (octets == 0) firstIs127 = (value == 127); ++octets; value = 0; digits = 0; } else if (c >= '0' && c <= '9') { value = value * 10 + (c - '0'); ++digits; } else { return false; } } return octets == 4 && firstIs127; } bool Connection::isLocalHost(const std::string& host) { std::string lowered = lowercase(host); if (!lowered.empty() && lowered.front() == '[' && lowered.back() == ']') { lowered = lowered.substr(1, lowered.size() - 2); } return lowered == "localhost" || lowered == "localhost." || lowered == "::1" || lowered == "0:0:0:0:0:0:0:1" || isExactIPv4Loopback(lowered); } bool Connection::usesPlaintextRemote(const ConnectionConfig& config) { return !config.use_tls && !isLocalHost(config.host); } bool Connection::allowsPlaintextRemote(const ConnectionConfig& config) { // Explicit opt-in (DRAGONX.conf: rpcallowplaintext=1) to send credentials over a plaintext // link to a remote host. Off by default — see usesPlaintextRemote(). return config.allow_plaintext_remote; } const char* Connection::authSourceName(AuthSource source) { switch (source) { case AuthSource::ConfigFile: return "config"; case AuthSource::Cookie: return "cookie"; case AuthSource::Missing: return "missing"; } return "unknown"; } bool Connection::createDefaultConfig(const std::string& path) { std::string rpcuser = generateSecureRandomString(16); std::string rpcpassword = generateSecureRandomString(32); if (rpcuser.empty() || rpcpassword.empty()) { DEBUG_LOGF("Failed to generate secure RPC credentials for config file: %s\n", path.c_str()); return false; } std::ofstream file(path); if (!file.is_open()) { DEBUG_LOGF("Failed to create config file: %s\n", path.c_str()); return false; } // Get the data directory for exportdir std::string dataDir = getDefaultDataDir(); file << "# DragonX configuration file\n"; file << "# Auto-generated by DragonX Wallet\n"; file << "\n"; file << "rpcuser=" << rpcuser << "\n"; file << "rpcpassword=" << rpcpassword << "\n"; file << "rpcport=" << DRAGONX_DEFAULT_RPC_PORT << "\n"; file << "server=1\n"; file << "txindex=1\n"; file << "exportdir=" << dataDir << "\n"; file << "experimentalfeatures=1\n"; file << "developerencryptwallet=1\n"; file << "addnode=node.dragonx.is\n"; file << "addnode=node1.dragonx.is\n"; file << "addnode=node2.dragonx.is\n"; file << "addnode=node3.dragonx.is\n"; file << "addnode=node4.dragonx.is\n"; file.close(); // The file holds the freshly-generated rpcuser/rpcpassword in plaintext. ofstream creates it // with the process umask (typically world-readable 0644), so restrict it to owner read/write // before another local user can read the credentials. { namespace fs = std::filesystem; std::error_code ec; fs::permissions(path, fs::perms::owner_read | fs::perms::owner_write, fs::perm_options::replace, ec); if (ec) DEBUG_LOGF("Could not restrict config permissions on %s: %s\n", path.c_str(), ec.message().c_str()); } DEBUG_LOGF("Created default config file: %s\n", path.c_str()); return true; } bool Connection::ensureExportDir(const std::string& confPath) { // Read existing config and check if exportdir is present std::ifstream inFile(confPath); if (!inFile.is_open()) { return false; } std::string contents; std::string line; bool hasExportDir = false; while (std::getline(inFile, line)) { contents += line + "\n"; // Check for exportdir (case insensitive check for key) if (line.find("exportdir=") == 0 || line.find("exportdir =") == 0) { hasExportDir = true; } } inFile.close(); if (hasExportDir) { return true; // Already has exportdir } // Append exportdir to config std::ofstream outFile(confPath, std::ios::app); if (!outFile.is_open()) { DEBUG_LOGF("Failed to open config for appending exportdir: %s\n", confPath.c_str()); return false; } std::string dataDir = getDefaultDataDir(); outFile << "\n# Export directory for wallet backups (required for z_exportwallet)\n"; outFile << "exportdir=" << dataDir << "\n"; outFile.close(); DEBUG_LOGF("Added exportdir to config: %s\n", confPath.c_str()); return true; } bool Connection::ensureEncryptionEnabled(const std::string& confPath) { // Read existing config and check if encryption flags are present std::ifstream inFile(confPath); if (!inFile.is_open()) { return false; } std::string contents; std::string line; bool hasExperimental = false; bool hasEncrypt = false; while (std::getline(inFile, line)) { contents += line + "\n"; if (line.find("experimentalfeatures=") == 0 || line.find("experimentalfeatures =") == 0) { hasExperimental = true; } if (line.find("developerencryptwallet=") == 0 || line.find("developerencryptwallet =") == 0) { hasEncrypt = true; } } inFile.close(); if (hasExperimental && hasEncrypt) { return true; // Already has both flags } // Append missing flags to config std::ofstream outFile(confPath, std::ios::app); if (!outFile.is_open()) { DEBUG_LOGF("Failed to open config for appending encryption flags: %s\n", confPath.c_str()); return false; } outFile << "\n# Enable wallet encryption support\n"; if (!hasExperimental) { outFile << "experimentalfeatures=1\n"; } if (!hasEncrypt) { outFile << "developerencryptwallet=1\n"; } outFile.close(); DEBUG_LOGF("Added encryption flags to config: %s\n", confPath.c_str()); return true; } bool Connection::readAuthCookie(const std::string& dataDir, std::string& user, std::string& password) { if (dataDir.empty()) return false; #ifdef _WIN32 std::string cookiePath = dataDir + "\\.cookie"; #else std::string cookiePath = dataDir + "/.cookie"; #endif std::ifstream file(cookiePath); if (!file.is_open()) return false; std::string cookie; std::getline(file, cookie); file.close(); // Cookie format: __cookie__:base64encodedpassword size_t colonPos = cookie.find(':'); if (colonPos == std::string::npos || colonPos == 0) return false; user = cookie.substr(0, colonPos); password = cookie.substr(colonPos + 1); // Trim \r if present (Windows line endings) while (!password.empty() && (password.back() == '\r' || password.back() == '\n')) { password.pop_back(); } if (user.empty() || password.empty()) return false; DEBUG_LOGF("Read auth cookie from: %s (user=%s)\n", cookiePath.c_str(), user.c_str()); return true; } } // namespace rpc } // namespace dragonx