From 2b191cea3473958378ae545c5654c592efa4ad5c Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 13 Jul 2026 20:44:25 -0500 Subject: [PATCH] fix(settings): securely wipe the exported private key (no heap-lingering secret) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Export Private Key dialog held the exported spending key in a plain std::string cleared only with .clear() — never sodium_memzero'd — and on outside-click/Esc the overlay's early return skipped even that, so the secret lingered in freed heap. Hold it in a fixed char[256] and sodium_memzero on every close path (Close button, outside-click/Esc early return, address change). Guard the async export callback so it can't populate the buffer after the dialog closed. Standalone security fix ahead of the settings-modal redesign (Phase 0a); the dialog's full restyle is Phase 2. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 27 +++++++++++++++++---------- src/app.h | 2 +- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index bc82151..626ee20 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -3226,6 +3226,10 @@ void App::renderExportKeyDialog() int btnFont = (int)dlgF("button-font", 1); if (!ui::material::BeginOverlayDialog("Export Private Key", &show_export_key_, dlgF("width", 600.0f), 0.94f)) { + // Closed via outside-click / Esc (the overload flips show_export_key_ then returns false here, + // so this is the only wipe point for that path) — scrub the exported secret. + sodium_memzero(export_result_, sizeof(export_result_)); + export_address_.clear(); return; } @@ -3247,7 +3251,7 @@ void App::renderExportKeyDialog() std::string display = util::truncateMiddle(addr, addrFrontLen, addrBackLen); if (ImGui::Selectable(display.c_str(), selected)) { export_address_ = addr; - export_result_.clear(); + sodium_memzero(export_result_, sizeof(export_result_)); } } ImGui::EndCombo(); @@ -3257,20 +3261,23 @@ void App::renderExportKeyDialog() if (ui::material::StyledButton("Export", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) { if (!export_address_.empty()) { exportPrivateKey(export_address_, [this](const std::string& key) { - export_result_ = key; + // Don't populate the secret buffer if the dialog was closed before the async + // export returned (it would otherwise linger un-wiped while the dialog is shut). + if (show_export_key_) + snprintf(export_result_, sizeof(export_result_), "%s", key.c_str()); }); } } - + ImGui::Spacing(); - - if (!export_result_.empty()) { + + if (export_result_[0]) { ImGui::Text("Private Key:"); - ImGui::InputTextMultiline("##exportresult", (char*)export_result_.c_str(), - export_result_.size() + 1, ImVec2(-1, dlgF("key-display-height", 60.0f)), ImGuiInputTextFlags_ReadOnly); - + ImGui::InputTextMultiline("##exportresult", export_result_, sizeof(export_result_), + ImVec2(-1, dlgF("key-display-height", 60.0f)), ImGuiInputTextFlags_ReadOnly); + if (ui::material::StyledButton("Copy to Clipboard", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) { - ImGui::SetClipboardText(export_result_.c_str()); + ImGui::SetClipboardText(export_result_); } } @@ -3281,7 +3288,7 @@ void App::renderExportKeyDialog() if (closeBtnFont < 0) closeBtnFont = btnFont; if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont(closeBtnFont))) { show_export_key_ = false; - export_result_.clear(); + sodium_memzero(export_result_, sizeof(export_result_)); export_address_.clear(); } diff --git a/src/app.h b/src/app.h index 47f8e15..4c99156 100644 --- a/src/app.h +++ b/src/app.h @@ -808,7 +808,7 @@ private: size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection) // Export/Import state - std::string export_result_; + char export_result_[256] = {0}; // SECRET exported key — fixed buffer so it can be sodium_memzero'd char import_key_input_[512] = {0}; std::string export_address_; std::string import_status_;