security(console): mask key-dump command RESULTS in the scrollback

The console redacted secret command ARGS but showed the OUTPUT of key-revealing
commands (dumpprivkey / z_exportkey / z_exportviewingkey / z_exportmnemonic /
dumpwallet / z_exportwallet, and the lite seed/export verbs) in full — visible
in the scrollback, Copy All, Ctrl+C and the filter. Now, when the submitted
command is key-revealing, its result is held ONLY in a member buffer (never in
the scrollback model, so Copy-All etc. can't leak it) and a masked placeholder
is shown. An explicit "Copy secret" button routes through the auto-clearing
clipboard; the buffer auto-wipes after 60s, on Clear, and on wallet lock
(via wipeDisplayedSecrets). The command string passed to the executor is
byte-identical. New pure helper ConsoleCommandRevealsSecret is unit-testable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 11:39:38 -05:00
co-authored by Claude Opus 4.8
parent 31e9c2ee2c
commit 83ce73f26b
6 changed files with 94 additions and 2 deletions
+13
View File
@@ -20,6 +20,8 @@
#include <vector>
#include <deque>
#include <mutex>
#include <functional>
#include <chrono>
namespace dragonx {
namespace ui {
@@ -66,6 +68,13 @@ public:
*/
void clear();
// Masked secret-result support: the plaintext of a key-revealing command's output is held ONLY here
// (never in the scrollback model), so Copy All / Ctrl+C / the filter can't leak it. An explicit
// "Copy secret" action routes through this handler (the app wires it to the auto-clearing clipboard).
void setCopySecretHandler(std::function<void(const std::string&)> f) { copy_secret_cb_ = std::move(f); }
bool hasSecretResult() const { return !secret_result_.empty(); }
void wipeSecretResult(); // zero + drop the buffer, disarm, reset the deadline (call on lock/clear)
// Ask the console to place the keyboard focus in the command input on the next render (consumed once).
// Called when the user switches to the Console tab, gated by the console_auto_focus setting.
void requestInputFocus() { focus_input_pending_ = true; }
@@ -162,6 +171,10 @@ private:
char input_buffer_[4096] = {0};
bool stop_confirm_pending_ = false; // 'stop' typed once, awaiting a confirming second 'stop'
std::string destructive_confirm_cmd_; // a destructive command typed once, awaiting an identical re-entry
bool secret_result_armed_ = false; // the just-submitted command reveals key material in its result
std::string secret_result_; // plaintext of the masked result (member-only; never in model_)
std::chrono::steady_clock::time_point secret_result_deadline_{}; // auto-wipe the buffer at this time
std::function<void(const std::string&)> copy_secret_cb_; // routes "Copy secret" to the app
bool focus_input_pending_ = false; // one-shot: focus the command input next render (tab-open auto-focus)
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)