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:
@@ -1235,6 +1235,14 @@ LiteImportResult LiteWalletController::importKey(std::string spendingOrViewingKe
|
||||
return importKeyCore(*bridge_, spendingOrViewingKey); // wipes the key
|
||||
}
|
||||
|
||||
// Recursively zero every string node of a parsed secret JSON tree (seed / private keys) so the parse
|
||||
// copies don't linger in freed heap. Best-effort (nlohmann's own lexer scratch isn't reachable).
|
||||
static void wipeJsonStrings(nlohmann::json& j)
|
||||
{
|
||||
if (j.is_string()) { secureWipeLiteSecret(j.get_ref<nlohmann::json::string_t&>()); }
|
||||
else if (j.is_object() || j.is_array()) { for (auto& el : j) wipeJsonStrings(el); }
|
||||
}
|
||||
|
||||
LiteExportResult LiteWalletController::exportPrivateKeys(const std::string& optionalAddress)
|
||||
{
|
||||
LiteExportResult out;
|
||||
@@ -1243,13 +1251,18 @@ LiteExportResult LiteWalletController::exportPrivateKeys(const std::string& opti
|
||||
return out;
|
||||
}
|
||||
// Empty address -> export keys for all addresses; otherwise just the given address.
|
||||
const auto result = bridge_->execute("export", optionalAddress);
|
||||
auto result = bridge_->execute("export", optionalAddress); // non-const: result.value is a secret
|
||||
if (!result.ok) {
|
||||
out.error = result.error.empty() ? "export failed" : result.error;
|
||||
return out;
|
||||
}
|
||||
nlohmann::json j;
|
||||
// result.value + the parse tree hold the private keys; wipe them on every return path. out already
|
||||
// has its own copy (out.privateKeysJson) which the caller wipes after use.
|
||||
struct Scrub { std::string& v; nlohmann::json& tree; ~Scrub() { secureWipeLiteSecret(v); wipeJsonStrings(tree); } }
|
||||
scrub{result.value, j};
|
||||
try {
|
||||
const auto j = nlohmann::json::parse(result.value);
|
||||
j = nlohmann::json::parse(result.value);
|
||||
if (j.is_object() && j.contains("error")) {
|
||||
out.error = extractJsonError(j);
|
||||
return out;
|
||||
@@ -1270,13 +1283,18 @@ LiteSeedResult LiteWalletController::exportSeed()
|
||||
out.error = "no wallet is open";
|
||||
return out;
|
||||
}
|
||||
const auto result = bridge_->execute("seed", "");
|
||||
auto result = bridge_->execute("seed", ""); // non-const: result.value holds the seed
|
||||
if (!result.ok) {
|
||||
out.error = result.error.empty() ? "seed export failed" : result.error;
|
||||
return out;
|
||||
}
|
||||
nlohmann::json j;
|
||||
// result.value + the parse tree contain the seed phrase; wipe them on every return path (out.seedPhrase
|
||||
// is extracted separately and returned to the caller).
|
||||
struct Scrub { std::string& v; nlohmann::json& tree; ~Scrub() { secureWipeLiteSecret(v); wipeJsonStrings(tree); } }
|
||||
scrub{result.value, j};
|
||||
try {
|
||||
const auto j = nlohmann::json::parse(result.value);
|
||||
j = nlohmann::json::parse(result.value);
|
||||
if (j.is_object()) {
|
||||
if (j.contains("error")) {
|
||||
out.error = extractJsonError(j);
|
||||
|
||||
Reference in New Issue
Block a user