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:
@@ -2484,11 +2484,16 @@ void ResetChatTab()
|
||||
s_show_blocked = false;
|
||||
// Drop the per-frame memoization caches so the next wallet doesn't briefly render the previous one's
|
||||
// conversations/thread (store.revision() is monotonic and would rebuild anyway, but be explicit).
|
||||
// Zero the decrypted plaintext the memo caches hold (conversation previews + full thread bodies)
|
||||
// before dropping them, so the previous wallet's messages don't linger in freed heap.
|
||||
auto wipeStr2 = [](std::string& s) { if (!s.empty()) sodium_memzero(&s[0], s.size()); s.clear(); };
|
||||
for (auto& c : s_convs) wipeStr2(c.lastBody);
|
||||
s_convs.clear();
|
||||
s_convsHidden = 0;
|
||||
s_convsKey = ~0ull;
|
||||
s_threadCid.clear();
|
||||
s_threadRev = ~0ull;
|
||||
for (auto& m : s_threadMsgs) { wipeStr2(m.body); wipeStr2(m.fail_reason); wipeStr2(m.peer_zaddr); }
|
||||
s_threadMsgs.clear();
|
||||
s_threadFlags.clear(); s_threadRows.clear(); s_threadHeights.clear();
|
||||
s_threadPrefix.assign(1, 0.0f); s_threadFailed.clear(); s_threadBodySizes.clear();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sodium.h>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "../layout.h" // Layout::dpiScale()
|
||||
@@ -15,19 +16,31 @@
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
// Best-effort zero of a seed-word vector (each word holds part of the phrase). Call after rendering.
|
||||
inline void WipeSeedWords(std::vector<std::string>& words)
|
||||
{
|
||||
for (auto& w : words) { if (!w.empty()) sodium_memzero(&w[0], w.size()); w.clear(); }
|
||||
words.clear();
|
||||
}
|
||||
|
||||
// Split a whitespace-separated seed phrase into its individual words.
|
||||
inline std::vector<std::string> SplitSeedWords(const std::string& phrase)
|
||||
{
|
||||
std::vector<std::string> words;
|
||||
words.reserve(32); // avoid reallocation stranding un-wiped moved-from copies in freed heap
|
||||
std::string cur;
|
||||
for (char c : phrase) {
|
||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
||||
if (!cur.empty()) { words.push_back(cur); cur.clear(); }
|
||||
if (!cur.empty()) {
|
||||
words.push_back(cur);
|
||||
sodium_memzero(&cur[0], cur.size()); // zero the scratch copy before reuse
|
||||
cur.clear();
|
||||
}
|
||||
} else {
|
||||
cur.push_back(c);
|
||||
}
|
||||
}
|
||||
if (!cur.empty()) words.push_back(cur);
|
||||
if (!cur.empty()) { words.push_back(cur); sodium_memzero(&cur[0], cur.size()); cur.clear(); }
|
||||
return words;
|
||||
}
|
||||
|
||||
@@ -45,6 +58,7 @@ inline void RenderSeedWordGrid(const std::vector<std::string>& words, float colS
|
||||
std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
|
||||
const float need = ImGui::CalcTextSize(cell).x + 20.0f * dp; // cell width + inter-column gap
|
||||
if (need > colStride) colStride = need;
|
||||
sodium_memzero(cell, sizeof(cell)); // the cell held a seed word
|
||||
}
|
||||
for (size_t i = 0; i < words.size(); ++i) {
|
||||
char cell[96];
|
||||
@@ -52,6 +66,7 @@ inline void RenderSeedWordGrid(const std::vector<std::string>& words, float colS
|
||||
ImGui::TextUnformatted(cell);
|
||||
if ((i % 4) != 3 && i + 1 < words.size())
|
||||
ImGui::SameLine(((i % 4) + 1) * colStride);
|
||||
sodium_memzero(cell, sizeof(cell));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user