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:
2026-06-10 14:00:45 -05:00
parent e40962cdf2
commit 094771af81
5 changed files with 58 additions and 4 deletions

View File

@@ -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<unsigned char>(*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;