diff --git a/src/app.cpp b/src/app.cpp index 2eacfb2..7060a25 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -566,6 +566,9 @@ void App::update() using RefreshTimer = services::NetworkRefreshService::Timer; network_refresh_.tick(io.DeltaTime); + // Wipe a secret (seed/private key) from the clipboard once its auto-clear delay elapses. + pumpSecretClipboardClear(); + // Full-node RPC refreshes gate on ACTUAL RPC connectivity, not state_.connected. In lite // builds state_.connected is the lite-wallet "online" proxy (true when a wallet is open, to // enable the wallet UI), but there is no RPC daemon — so RPC polls (mining/balance/peers/txs) @@ -2072,7 +2075,7 @@ void App::renderLiteFirstRunPrompt() step = 2; } ImGui::SameLine(); - if (ImGui::Button("Copy", ImVec2(80, 0))) ImGui::SetClipboardText(seed.c_str()); + if (ImGui::Button("Copy", ImVec2(80, 0))) copySecretToClipboard(seed); ImGui::SameLine(); if (ImGui::Button("Skip", ImVec2(80, 0))) { ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f); @@ -3659,6 +3662,33 @@ std::string App::transactionRefreshProgressText() const return TR("tx_loading_fetching_transparent"); } +void App::copySecretToClipboard(const std::string& secret) +{ + ImGui::SetClipboardText(secret.c_str()); + // Keep only an FNV-1a hash of the secret so we can later confirm the clipboard still holds it + // (and shouldn't be clobbered) WITHOUT retaining the plaintext. + std::uint64_t h = 1469598103934665603ULL; + for (unsigned char c : secret) h = (h ^ c) * 1099511628211ULL; + clipboard_secret_hash_ = secret.empty() ? 0 : h; + clipboard_clear_deadline_ = secret.empty() ? 0.0 : (ImGui::GetTime() + 45.0); + if (!secret.empty()) + ui::Notifications::instance().info("Copied — clipboard auto-clears in 45s", 4.0f); +} + +void App::pumpSecretClipboardClear() +{ + if (clipboard_clear_deadline_ <= 0.0) return; + if (ImGui::GetTime() < clipboard_clear_deadline_) return; + // Only clear if the clipboard STILL holds our secret (the user may have copied something else). + if (const char* cb = ImGui::GetClipboardText()) { + std::uint64_t h = 1469598103934665603ULL; + for (const char* p = cb; *p; ++p) h = (h ^ static_cast(*p)) * 1099511628211ULL; + if (h == clipboard_secret_hash_) ImGui::SetClipboardText(""); + } + clipboard_clear_deadline_ = 0.0; + clipboard_secret_hash_ = 0; +} + void App::maybeFinishTransactionSendProgress() { using Job = services::NetworkRefreshService::Job; diff --git a/src/app.h b/src/app.h index a80485e..7324b39 100644 --- a/src/app.h +++ b/src/app.h @@ -389,6 +389,13 @@ public: bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); } std::string transactionSendProgressText() const; std::string transactionRefreshProgressText() const; + + // Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a + // short delay the clipboard is wiped IF it still holds this secret (so we don't clobber + // something the user copied afterwards). Only a hash of the secret is retained, never the + // plaintext. Call pumpSecretClipboardClear() each frame to action the clear. + void copySecretToClipboard(const std::string& secret); + void pumpSecretClipboardClear(); bool isTransactionRefreshInProgress() const { return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions); } @@ -512,6 +519,9 @@ private: int daemon_wait_attempts_ = 0; bool daemon_start_error_shown_ = false; int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once + // Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept. + std::uint64_t clipboard_secret_hash_ = 0; + double clipboard_clear_deadline_ = 0.0; float loading_timer_ = 0.0f; // spinner animation for loading overlay // Current page (sidebar navigation) diff --git a/src/rpc/connection.cpp b/src/rpc/connection.cpp index b6acd49..276cbaa 100644 --- a/src/rpc/connection.cpp +++ b/src/rpc/connection.cpp @@ -331,7 +331,18 @@ bool Connection::createDefaultConfig(const std::string& path) 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; } diff --git a/src/rpc/rpc_client.cpp b/src/rpc/rpc_client.cpp index 18f2fdf..58d47f1 100644 --- a/src/rpc/rpc_client.cpp +++ b/src/rpc/rpc_client.cpp @@ -11,6 +11,7 @@ #include "../util/base64.h" #include +#include #include #include #include @@ -145,9 +146,11 @@ bool RPCClient::connect(const std::string& host, const std::string& port, port_ = port; last_connect_info_ = json(); - // Create Basic auth header with proper base64 encoding + // Create Basic auth header with proper base64 encoding, then wipe the plaintext + // "user:password" temporary (std::string does not zero its buffer on destruction). std::string credentials = user + ":" + password; auth_ = util::base64_encode(credentials); + if (!credentials.empty()) sodium_memzero(credentials.data(), credentials.size()); impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/"; VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str()); diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index e0ac9cd..bf3e4da 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1709,7 +1709,7 @@ void RenderSettingsPage(App* app) { } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); if (TactileButton("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) { - ImGui::SetClipboardText(s_settingsState.lite_export_secret.c_str()); + app->copySecretToClipboard(s_settingsState.lite_export_secret); } ImGui::SameLine(0, Layout::spacingSm()); // Save the seed (+ birthday) to an owner-only (0600) file in the config dir.