fix(security): throttle and wipe the import dialog clipboard preview

Hovering the Paste button in the import-key dialog read the OS clipboard
every frame to draw a ghost preview of the key, and the trimmed
plaintext was rebuilt each frame and never wiped. SDL's clipboard read
is a synchronous round-trip, and the pasted value is a secret private or
viewing key.

- The clipboard is now read only on hover entry and then at most twice a
  second, and the classified preview is cached rather than recomputed
  every frame.
- The cached preview is a dedicated secret buffer: it is wiped the moment
  it stops being shown, wiped on secret wipe, and the transient trimmed
  copy is zeroed right after it is classified.
- Trimming moves to a shared clipboard helper shared with the paste path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 17:42:30 -05:00
co-authored by Claude Opus 4.8
parent d601f786e2
commit 1cd4ea4026
2 changed files with 52 additions and 24 deletions
+48 -24
View File
@@ -148,6 +148,7 @@ void App::wipeSecrets()
sodium_memzero(&seed_backup_phrase_[0], seed_backup_phrase_.size());
sodium_memzero(export_result_, sizeof(export_result_)); // exported WIF/z-key (SECRET)
sodium_memzero(import_key_input_, sizeof(import_key_input_)); // pasted private key (SECRET)
sodium_memzero(import_clip_preview_, sizeof(import_clip_preview_)); // clipboard preview (SECRET)
}
// Enqueue a blocking xmrig start/stop op onto the dedicated serialized control thread so the render thread
@@ -3958,9 +3959,7 @@ void App::renderImportKeyDialog()
if (pasteClicked) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
std::string trimmed(clipboard);
while (!trimmed.empty() && (trimmed.front()==' '||trimmed.front()=='\t'||trimmed.front()=='\n'||trimmed.front()=='\r')) trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back()==' '||trimmed.back()=='\t'||trimmed.back()=='\n'||trimmed.back()=='\r')) trimmed.pop_back();
std::string trimmed = dragonx::util::trimmedClipboardText(clipboard);
snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str());
}
}
@@ -3972,27 +3971,52 @@ void App::renderImportKeyDialog()
// wrong-type / red unrecognized). Drawn via the draw list only: it verifies the key WITHOUT adding
// a layout row, since a hover-toggled row would grow the centered dialog and shift the button out
// from under the cursor (hover flicker).
if (pasteHovered && import_key_input_[0] == '\0') {
const char* clip = ImGui::GetClipboardText();
if (clip && clip[0]) {
std::string t(clip);
while (!t.empty() && (t.front()==' '||t.front()=='\t'||t.front()=='\n'||t.front()=='\r')) t.erase(t.begin());
while (!t.empty() && (t.back()==' '||t.back()=='\t'||t.back()=='\n'||t.back()=='\r')) t.pop_back();
if (!t.empty()) {
const bool pView = services::WalletSecurityController::isViewingKey(t);
const bool pSpend = services::WalletSecurityController::isRecognizedPrivateKey(t);
const bool pOk = viewMode ? pView : pSpend;
const bool pWrong = viewMode ? pSpend : pView;
ImVec4 col = pOk ? m::SuccessVec4()
: pWrong ? ImVec4(0.8f, 0.6f, 0.0f, 1.0f)
: m::ErrorVec4();
col.w = 0.6f; // ghost (uncommitted) preview
std::string shown = import_key_reveal_ ? t : std::string(std::min(t.size(), (size_t)56), '*');
if (shown.size() > 48) shown.resize(48); // fits the field before the flush eye button
const ImVec2 tp(fieldMin.x + ImGui::GetStyle().FramePadding.x,
fieldMin.y + ImGui::GetStyle().FramePadding.y);
ImGui::GetWindowDrawList()->AddText(tp, ImGui::ColorConvertFloat4ToU32(col), shown.c_str());
}
const double nowClip = ImGui::GetTime();
const bool hoverEnter = pasteHovered && !import_clip_hover_prev_;
import_clip_hover_prev_ = pasteHovered;
if (pasteHovered && import_key_input_[0] == '\0' &&
(hoverEnter || nowClip >= import_clip_next_at_)) {
import_clip_next_at_ = nowClip + 0.5; // SDL clipboard is a synchronous round-trip
// Copy immediately - the backend buffer is freed on the next GetClipboardText() call.
std::string t = dragonx::util::trimmedClipboardText(ImGui::GetClipboardText());
if (!t.empty()) {
const bool pView = services::WalletSecurityController::isViewingKey(t);
const bool pSpend = services::WalletSecurityController::isRecognizedPrivateKey(t);
const bool pOk = viewMode ? pView : pSpend;
const bool pWrong = viewMode ? pSpend : pView;
import_clip_class_ = pOk ? 1 : pWrong ? 2 : 3;
snprintf(import_clip_preview_, sizeof(import_clip_preview_), "%.48s", t.c_str());
sodium_memzero(&t[0], t.size()); // wipe the transient clipboard copy
} else {
import_clip_class_ = 0;
sodium_memzero(import_clip_preview_, sizeof(import_clip_preview_));
}
}
if (pasteHovered && import_key_input_[0] == '\0' && import_clip_class_ != 0) {
ImVec4 col = import_clip_class_ == 1 ? m::SuccessVec4()
: import_clip_class_ == 2 ? ImVec4(0.8f, 0.6f, 0.0f, 1.0f)
: m::ErrorVec4();
col.w = 0.6f; // ghost (uncommitted) preview
const size_t plen = std::strlen(import_clip_preview_);
const ImVec2 tp(fieldMin.x + ImGui::GetStyle().FramePadding.x,
fieldMin.y + ImGui::GetStyle().FramePadding.y);
if (import_key_reveal_) {
ImGui::GetWindowDrawList()->AddText(tp, ImGui::ColorConvertFloat4ToU32(col),
import_clip_preview_); // revealed: no copy
} else {
std::string stars(plen, '*'); // not secret
ImGui::GetWindowDrawList()->AddText(tp, ImGui::ColorConvertFloat4ToU32(col),
stars.c_str());
}
}
// Wipe the cache the moment it is no longer shown.
if (!pasteHovered || import_key_input_[0] != '\0') {
if (import_clip_class_ != 0) {
sodium_memzero(import_clip_preview_, sizeof(import_clip_preview_));
import_clip_class_ = 0;
}
}
+4
View File
@@ -1499,6 +1499,10 @@ private:
bool export_in_progress_ = false; // async key fetch running (spinner + disable Export)
bool export_error_ = false; // last export returned no key (locked wallet / failure)
char import_key_input_[512] = {0};
char import_clip_preview_[64] = {0}; // trimmed clipboard preview (<=48 chars + NUL); SECRET, wiped
uint8_t import_clip_class_ = 0; // 0 none / 1 ok / 2 wrong-type / 3 unrecognized
double import_clip_next_at_ = 0.0; // ImGui::GetTime() of next allowed clipboard read
bool import_clip_hover_prev_ = false; // hover edge
std::string export_address_;
std::string import_status_;
bool import_success_ = false;