security: wipe RPC creds, lock down generated conf, auto-clear secret clipboard (audit #4-6)
- rpc_client: wipe the plaintext "user:password" temporary with sodium_memzero after base64-encoding it into the auth header (std::string doesn't zero its buffer on destruction). - connection: the auto-generated DRAGONX.conf holds rpcuser/rpcpassword in plaintext but was written with the default umask (often world-readable 0644). Restrict it to owner read/write after creation so another local user can't read the credentials. - app: copying a seed phrase / private key to the clipboard now arms an auto-clear — App::copySecretToClipboard() copies the secret and, after 45s, wipes the clipboard IF it still holds that secret (compared via a stored hash, never the plaintext). Wired into the lite first-run wizard's seed Copy and the Settings export-secret Copy, with a "clipboard auto-clears in 45s" notice. pumpSecretClipboardClear() runs each frame. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
32
src/app.cpp
32
src/app.cpp
@@ -566,6 +566,9 @@ void App::update()
|
|||||||
using RefreshTimer = services::NetworkRefreshService::Timer;
|
using RefreshTimer = services::NetworkRefreshService::Timer;
|
||||||
network_refresh_.tick(io.DeltaTime);
|
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
|
// 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
|
// 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)
|
// 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;
|
step = 2;
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
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();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Skip", ImVec2(80, 0))) {
|
if (ImGui::Button("Skip", ImVec2(80, 0))) {
|
||||||
ui::Notifications::instance().success(TR("lite_welcome_created"), 6.0f);
|
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");
|
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<unsigned char>(*p)) * 1099511628211ULL;
|
||||||
|
if (h == clipboard_secret_hash_) ImGui::SetClipboardText("");
|
||||||
|
}
|
||||||
|
clipboard_clear_deadline_ = 0.0;
|
||||||
|
clipboard_secret_hash_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void App::maybeFinishTransactionSendProgress()
|
void App::maybeFinishTransactionSendProgress()
|
||||||
{
|
{
|
||||||
using Job = services::NetworkRefreshService::Job;
|
using Job = services::NetworkRefreshService::Job;
|
||||||
|
|||||||
10
src/app.h
10
src/app.h
@@ -389,6 +389,13 @@ public:
|
|||||||
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
||||||
std::string transactionSendProgressText() const;
|
std::string transactionSendProgressText() const;
|
||||||
std::string transactionRefreshProgressText() 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 {
|
bool isTransactionRefreshInProgress() const {
|
||||||
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
||||||
}
|
}
|
||||||
@@ -512,6 +519,9 @@ private:
|
|||||||
int daemon_wait_attempts_ = 0;
|
int daemon_wait_attempts_ = 0;
|
||||||
bool daemon_start_error_shown_ = false;
|
bool daemon_start_error_shown_ = false;
|
||||||
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
|
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
|
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
||||||
|
|
||||||
// Current page (sidebar navigation)
|
// Current page (sidebar navigation)
|
||||||
|
|||||||
@@ -331,7 +331,18 @@ bool Connection::createDefaultConfig(const std::string& path)
|
|||||||
file << "addnode=node4.dragonx.is\n";
|
file << "addnode=node4.dragonx.is\n";
|
||||||
|
|
||||||
file.close();
|
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());
|
DEBUG_LOGF("Created default config file: %s\n", path.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "../util/base64.h"
|
#include "../util/base64.h"
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
#include <sodium.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -145,9 +146,11 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
|||||||
port_ = port;
|
port_ = port;
|
||||||
last_connect_info_ = json();
|
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;
|
std::string credentials = user + ":" + password;
|
||||||
auth_ = util::base64_encode(credentials);
|
auth_ = util::base64_encode(credentials);
|
||||||
|
if (!credentials.empty()) sodium_memzero(credentials.data(), credentials.size());
|
||||||
|
|
||||||
impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/";
|
impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/";
|
||||||
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
|
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
|
||||||
|
|||||||
@@ -1709,7 +1709,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
if (TactileButton("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) {
|
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());
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
// Save the seed (+ birthday) to an owner-only (0600) file in the config dir.
|
// Save the seed (+ birthday) to an owner-only (0600) file in the config dir.
|
||||||
|
|||||||
Reference in New Issue
Block a user