fix: security-audit remediation (15 findings), empty-wallet warning, and send/chat/console/shutdown UX
Security audit remediation (15 confirmed findings from the codebase audit): - H-02: scrub+delete the decrypt-flow plaintext key export on ALL exit paths (RAII guard) and purge stale obsidiandecryptexport* files at startup. - M-01/L-03/L-04/L-05/L-07: sodium_memzero the Set-PIN and encrypt-PIN worker passphrase/PIN copies, the RPC Basic-auth string (auth_), the exported/imported key buffers (App::wipeSecrets, called from ~App and before main's _Exit), and the first-run wizard "Skip" buffers. - M-03/M-04/M-05/L-06: return locked COPIES from XmrigManager/EmbeddedDaemon getters (dedicated error_mutex_; DaemonController::lastError now by value), route xmrig last_error_ writes through a locked setter, and wrap shutdown_status_/wizard_stop_status_ in a locking GuardedStatus (wizard_stopping_external_ -> std::atomic). - M-02: persist after a console send/shield/import in the lite backend. - L-01: require the confirm click for z_shieldcoinbase/z_mergetoaddress. - L-02: quote/escape each Windows daemon argv per the MSDN CommandLineToArgvW rules. - L-08: pin json/tomlplusplus/libwebp FetchContent to immutable commit SHAs. - I-01: extract updater archives from the already-verified in-memory buffer (no disk re-read TOCTOU). Feature: warn once (full-node) when the active wallet loads empty while a sibling wallet file in the datadir holds keys. A funded salvage wallet.<ts>.bak routes to the recovery/Restore flow; a funded sibling .dat routes to the wallet manager. Per-wallet-file dismissal; gated on synced + address-list-loaded to avoid false positives on warm reconnect / spent-down wallets. UX fixes: - send: show the TOTAL balance (with a spendable "available" note) in the source dropdown and keep pending-change addresses visible. - chat: insert emoji at the cursor position; restrict new-chat recipients to shielded (z) addresses. - console: optional auto-focus of the command input on tab open (off by default). - shutdown: when "stop external daemon" is on, keep the shutdown screen up until the external node actually exits, showing live status. Adversarially reviewed; verified across full-node, lite, and Windows builds; tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,14 +71,28 @@ enum class EncryptDialogPhase {
|
||||
Done // Finished — close dialog
|
||||
};
|
||||
|
||||
// A status string written by a background/worker thread and read every frame by the UI thread. Its
|
||||
// operator= locks, so all the plain `x = "..."` assignment sites stay unchanged; readers call get()
|
||||
// for a consistent per-frame snapshot instead of racing a non-atomic std::string. (M-05, L-06)
|
||||
class GuardedStatus {
|
||||
public:
|
||||
GuardedStatus() = default;
|
||||
GuardedStatus& operator=(std::string v) { std::lock_guard<std::mutex> lk(m_); v_ = std::move(v); return *this; }
|
||||
std::string get() const { std::lock_guard<std::mutex> lk(m_); return v_; }
|
||||
private:
|
||||
mutable std::mutex m_;
|
||||
std::string v_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Main application class
|
||||
*
|
||||
*
|
||||
* Manages application state, RPC connection, and coordinates UI rendering.
|
||||
*/
|
||||
class App {
|
||||
public:
|
||||
App();
|
||||
void wipeSecrets(); // scrub all resident secret buffers; called from ~App() AND the forced-exit path (L-05)
|
||||
~App();
|
||||
|
||||
// Non-copyable
|
||||
@@ -798,6 +812,10 @@ private:
|
||||
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
|
||||
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
|
||||
void maybeRemindSeedBackup();
|
||||
void maybeWarnEmptyWalletWithFundedSiblings(); // full-node: empty active wallet + a funded sibling → warn once
|
||||
void scanFundedSiblingsAsync(); // off-UI-thread probe of sibling wallet files
|
||||
static void scrubAndRemoveExport(const std::string& path); // zero + delete a plaintext key export (H-02)
|
||||
void sweepStaleDecryptExports(); // startup net: purge stale obsidiandecryptexport* files (H-02)
|
||||
|
||||
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
|
||||
void beginCreateSeedWallet(); // starts the isolated create on a background thread
|
||||
@@ -869,7 +887,7 @@ private:
|
||||
std::atomic<bool> shutting_down_{false};
|
||||
std::atomic<bool> shutdown_complete_{false};
|
||||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||||
std::string shutdown_status_;
|
||||
GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05)
|
||||
std::thread shutdown_thread_;
|
||||
float shutdown_timer_ = 0.0f;
|
||||
bool force_quit_confirm_ = false;
|
||||
@@ -906,6 +924,16 @@ private:
|
||||
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
||||
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
||||
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
||||
// Complementary on-disk safety net for a salvage we DIDN'T witness this launch (happened on a prior
|
||||
// run, or under an external daemon whose startup output we never captured): if the active wallet loads
|
||||
// empty while a sibling wallet file in the datadir still holds keys, warn once so the user's funds
|
||||
// (likely in a wallet.<ts>.bak) aren't mistaken for loss. See maybeWarnEmptyWalletWithFundedSiblings().
|
||||
struct FundedSibling { std::string fileName; int transparentKeys = 0; int shieldedKeys = 0; };
|
||||
bool show_empty_wallet_warning_ = false; // auto-shown warning modal
|
||||
bool empty_wallet_warn_checked_ = false; // evaluated this wallet-open already (reset in onConnected)
|
||||
bool empty_wallet_scan_in_flight_ = false; // a sibling scan is running (main-thread only)
|
||||
bool empty_wallet_has_salvage_bak_ = false; // modal variant: a funded salvage .bak → offer Restore
|
||||
std::vector<FundedSibling> empty_wallet_funded_siblings_; // scan result (main-thread only)
|
||||
// The recovery dialog is the ONE authoritative surface: it stays open through the async rebuild/
|
||||
// restore, driven Offer → Working → Done/Failed (pumpWalletRestore sets the outcome). Presentation
|
||||
// only — the fund-safety file ops in rebuildWalletDatabase()/restoreOriginalWallet() are unchanged.
|
||||
@@ -1264,8 +1292,8 @@ private:
|
||||
services::WalletSecurityWorkflow wallet_security_workflow_;
|
||||
|
||||
// Wizard: stopping an external daemon before bootstrap
|
||||
bool wizard_stopping_external_ = false;
|
||||
std::string wizard_stop_status_;
|
||||
std::atomic<bool> wizard_stopping_external_{false}; // written by the stop worker, read by the UI (L-06)
|
||||
GuardedStatus wizard_stop_status_; // thread-safe: written by the stop worker, read by the UI (L-06)
|
||||
|
||||
// PIN vault
|
||||
std::unique_ptr<util::SecureVault> vault_;
|
||||
@@ -1358,6 +1386,7 @@ private:
|
||||
void renderBlockDbReindexDialog(); // offer to rebuild an unreadable block database (-reindex)
|
||||
void reindexBlockDatabase(); // restart the daemon with -reindex to rebuild the block DB
|
||||
void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat
|
||||
void renderEmptyWalletWarningDialog();// warn that the active wallet is empty while a sibling holds funds
|
||||
void detectWalletAutoRecovery(); // scan daemon output for a salvage; fire the warning once/session
|
||||
void restoreOriginalWallet(); // swap the wallet.<ts>.bak back over the salvaged copy + restart
|
||||
void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result
|
||||
|
||||
Reference in New Issue
Block a user