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:
+48
-24
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user