security(hygiene): best-effort scrubbing of residual in-memory secret copies (Tier 4)
Defense-in-depth against a local attacker / core dump reading un-zeroed heap copies of secrets (threat model is local, not remote). Best-effort by nature — std::string SSO/reallocation and ImGui buffer growth can still strand copies — so these shrink the exposure window rather than guarantee absence: - rpc_client performCall: a scope guard now sodium_memzeroes the request body + payload tree (walletpassphrase / *_importkey args) after the call and detaches CURLOPT_POSTFIELDS. Request content sent to curl is unchanged. - New ui::scrubImGuiInputTextState() zeroes ImGui's retained InputText buffers (live/revert/callback/deactivated) — the app memzeroed its own char[]s but ImGui kept copies. Called from wipeSecrets()/wipeDisplayedSecrets(). PINNED to ImGui 1.92.6 via a #error guard so a vendored bump forces a re-audit (the stb undo buffer is an incomplete type in the header, so it's left as a noted gap). - Lite exportSeed/exportPrivateKeys wipe result.value + the parsed JSON tree. - Seed-word grid (SplitSeedWords/RenderSeedWordGrid) zeroes its per-frame word and cell copies; the wizard word/chip vectors are zeroed before clearing. - ChatStore::clear() + ResetChatTab() zero decrypted message bodies on lock/switch/teardown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+14
-5
@@ -40,6 +40,7 @@
|
||||
#include "ui/windows/faq_dialog.h"
|
||||
#include "embedded/IconsMaterialDesign.h"
|
||||
#include "ui/windows/key_export_dialog.h"
|
||||
#include "ui/imgui_secret_scrub.h" // scrubImGuiInputTextState — zero ImGui's retained secret buffers
|
||||
#include "ui/windows/transaction_details_dialog.h"
|
||||
#include "ui/windows/qr_popup_dialog.h"
|
||||
#include "ui/windows/validate_address_dialog.h"
|
||||
@@ -143,6 +144,7 @@ void App::wipeSecrets()
|
||||
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)
|
||||
ui::scrubImGuiInputTextState(); // zero ImGui's retained copies of whatever secret was last typed
|
||||
}
|
||||
|
||||
// Scrub + close any secret that is currently ON SCREEN (revealed seed phrase, migration seed, exported
|
||||
@@ -163,6 +165,7 @@ void App::wipeDisplayedSecrets()
|
||||
sodium_memzero(export_result_, sizeof(export_result_));
|
||||
ui::KeyExportDialog::hide(); // zeros its static s_key via secureWipeLiteSecret
|
||||
console_tab_.wipeSecretResult(); // drop any masked key-dump result held in the console
|
||||
ui::scrubImGuiInputTextState(); // zero ImGui's retained copy of the last-typed passphrase/PIN
|
||||
}
|
||||
|
||||
// Enqueue a blocking xmrig start/stop op onto the dedicated serialized control thread so the render thread
|
||||
@@ -3486,14 +3489,20 @@ void App::renderLiteFirstRunPrompt()
|
||||
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x + vp->Size.x * 0.5f, vp->Pos.y + vp->Size.y * 0.5f),
|
||||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||
|
||||
// Best-effort zero of the seed-word statics (they hold the whole phrase) before they're dropped.
|
||||
auto wipeStr = [](std::string& s) { if (!s.empty()) sodium_memzero(&s[0], s.size()); s.clear(); };
|
||||
auto wipeWords = [&](std::vector<std::string>& v) { for (auto& w : v) wipeStr(w); v.clear(); };
|
||||
auto wipeChips = [&](std::vector<std::pair<std::string, bool>>& v) {
|
||||
for (auto& c : v) { wipeStr(c.first); c.second = false; } v.clear();
|
||||
};
|
||||
auto finish = [&]() {
|
||||
wallet::secureWipeLiteSecret(seed);
|
||||
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
||||
restoreBirthday = 0;
|
||||
restoring = false;
|
||||
restoreErr.clear();
|
||||
words.clear();
|
||||
chips.clear();
|
||||
wipeWords(words);
|
||||
wipeChips(chips);
|
||||
progress = 0;
|
||||
step = 0;
|
||||
skipConfirm = false;
|
||||
@@ -3601,7 +3610,7 @@ void App::renderLiteFirstRunPrompt()
|
||||
ImGui::Spacing(); ImGui::Spacing();
|
||||
|
||||
if (ui::material::TactileButton(TR("appx_ive_written_it_down"), ImVec2(btnW, 0))) {
|
||||
chips.clear();
|
||||
wipeChips(chips);
|
||||
for (const auto& w : words) chips.emplace_back(w, false);
|
||||
std::mt19937 rng{std::random_device{}()};
|
||||
std::shuffle(chips.begin(), chips.end(), rng);
|
||||
@@ -4734,7 +4743,7 @@ void App::renderSeedBackupDialog()
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", TR("seed_backup_intro"));
|
||||
ImGui::Spacing();
|
||||
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_backup_phrase_));
|
||||
{ auto words = ui::SplitSeedWords(seed_backup_phrase_); ui::RenderSeedWordGrid(words); ui::WipeSeedWords(words); }
|
||||
ImGui::Spacing();
|
||||
if (!seed_backup_status_.empty()) {
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
@@ -4904,7 +4913,7 @@ void App::renderSeedMigrationDialog()
|
||||
// distinct artifact, not body copy. Purely visual — the words/derivation/logic are untouched.
|
||||
{
|
||||
ui::material::GlassSectionScope seedPanel;
|
||||
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_migration_seed_));
|
||||
{ auto words = ui::SplitSeedWords(seed_migration_seed_); ui::RenderSeedWordGrid(words); ui::WipeSeedWords(words); }
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr"));
|
||||
|
||||
Reference in New Issue
Block a user