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:
27
src/app.cpp
27
src/app.cpp
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
Reference in New Issue
Block a user