fix(settings): securely wipe the exported private key (no heap-lingering secret)

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 20:44:25 -05:00
parent 6ecbf835ec
commit 2b191cea34
2 changed files with 18 additions and 11 deletions

View File

@@ -3226,6 +3226,10 @@ void App::renderExportKeyDialog()
int btnFont = (int)dlgF("button-font", 1); int btnFont = (int)dlgF("button-font", 1);
if (!ui::material::BeginOverlayDialog("Export Private Key", &show_export_key_, dlgF("width", 600.0f), 0.94f)) { 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; return;
} }
@@ -3247,7 +3251,7 @@ void App::renderExportKeyDialog()
std::string display = util::truncateMiddle(addr, addrFrontLen, addrBackLen); std::string display = util::truncateMiddle(addr, addrFrontLen, addrBackLen);
if (ImGui::Selectable(display.c_str(), selected)) { if (ImGui::Selectable(display.c_str(), selected)) {
export_address_ = addr; export_address_ = addr;
export_result_.clear(); sodium_memzero(export_result_, sizeof(export_result_));
} }
} }
ImGui::EndCombo(); ImGui::EndCombo();
@@ -3257,20 +3261,23 @@ void App::renderExportKeyDialog()
if (ui::material::StyledButton("Export", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) { if (ui::material::StyledButton("Export", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) {
if (!export_address_.empty()) { if (!export_address_.empty()) {
exportPrivateKey(export_address_, [this](const std::string& key) { 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(); ImGui::Spacing();
if (!export_result_.empty()) { if (export_result_[0]) {
ImGui::Text("Private Key:"); ImGui::Text("Private Key:");
ImGui::InputTextMultiline("##exportresult", (char*)export_result_.c_str(), ImGui::InputTextMultiline("##exportresult", export_result_, sizeof(export_result_),
export_result_.size() + 1, ImVec2(-1, dlgF("key-display-height", 60.0f)), ImGuiInputTextFlags_ReadOnly); ImVec2(-1, dlgF("key-display-height", 60.0f)), ImGuiInputTextFlags_ReadOnly);
if (ui::material::StyledButton("Copy to Clipboard", ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) { 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 (closeBtnFont < 0) closeBtnFont = btnFont;
if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont(closeBtnFont))) { if (ui::material::StyledButton("Close", ImVec2(dlgF("close-button-width", 120.0f), 0), ui::material::resolveButtonFont(closeBtnFont))) {
show_export_key_ = false; show_export_key_ = false;
export_result_.clear(); sodium_memzero(export_result_, sizeof(export_result_));
export_address_.clear(); export_address_.clear();
} }

View File

@@ -808,7 +808,7 @@ private:
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection) size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
// Export/Import state // 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}; char import_key_input_[512] = {0};
std::string export_address_; std::string export_address_;
std::string import_status_; std::string import_status_;