diff --git a/src/app_network.cpp b/src/app_network.cpp index fb4ccbc..a7c9393 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -2632,7 +2632,7 @@ void App::exportPrivateKey(const std::string& address, std::functioncall(method, {address}).get(); + key = rpc_->callSecret(method, {address}).get(); // zero the raw body too (B7) } catch (const std::exception& e) { err = e.what(); } @@ -2758,7 +2758,7 @@ void App::maybeProvisionChatIdentity() bool unavailable = false; // no usable secret — stop retrying this session rpc::RPCClient::TraceScope trace("HushChat / identity"); try { - auto response = rpc_->call("z_exportmnemonic"); + auto response = rpc_->callSecret("z_exportmnemonic"); // zero the raw body too (B7) if (response.contains("mnemonic") && response["mnemonic"].is_string()) { // Scrub the json's own copy of the seed after taking ours (the rest of the RPC // response chain is unmanaged — a fuller fix belongs in the RPC layer). @@ -2774,7 +2774,7 @@ void App::maybeProvisionChatIdentity() try { // Mirror the mnemonic path: take our copy, then scrub the json's own copy of the // spending key so it isn't left in freed heap (B4). z_exportkey returns a bare string. - auto keyResp = rpc_->call("z_exportkey", {fallbackZaddr}); + auto keyResp = rpc_->callSecret("z_exportkey", {fallbackZaddr}); // zero the raw body too (B7) if (keyResp.is_string()) { auto& key = keyResp.get_ref(); secret = key; @@ -3374,7 +3374,7 @@ void App::exportSeedPhrase(std::functioncall("z_exportmnemonic"); + auto response = rpc_->callSecret("z_exportmnemonic"); // zero the raw body too (B7) if (response.contains("mnemonic") && response["mnemonic"].is_string()) { auto& m = response["mnemonic"].get_ref(); phrase = m; diff --git a/src/daemon/seed_wallet_creator.cpp b/src/daemon/seed_wallet_creator.cpp index bcad883..9d0d992 100644 --- a/src/daemon/seed_wallet_creator.cpp +++ b/src/daemon/seed_wallet_creator.cpp @@ -110,7 +110,7 @@ SeedWalletResult SeedWalletCreator::create(bool keepDatadir, // 6. Export the new seed phrase + a fresh shielded receive address (the future sweep target). try { - auto m = cli.call("z_exportmnemonic"); + auto m = cli.callSecret("z_exportmnemonic"); // zero the raw body too (B7) if (m.contains("mnemonic") && m["mnemonic"].is_string()) r.seedPhrase = m["mnemonic"].get(); r.destAddress = cli.call("z_getnewaddress").get(); diff --git a/src/rpc/rpc_client.cpp b/src/rpc/rpc_client.cpp index 0aa931a..ee9df39 100644 --- a/src/rpc/rpc_client.cpp +++ b/src/rpc/rpc_client.cpp @@ -370,6 +370,27 @@ json RPCClient::call(const std::string& method, const json& params) return parseRpcResult(http_code, response_data); } +json RPCClient::callSecret(const std::string& method, const json& params) +{ + std::lock_guard lk(curl_mutex_); + if (!impl_->curl) { + throw std::runtime_error("Not connected"); + } + + // Zero the raw response body whether parsing succeeds or throws — it holds the secret in the + // clear (the curl write buffer performCall returns), and would otherwise be freed un-wiped (B7). + long http_code = 0; + std::string response_data = performCall(method, params, http_code); + try { + json result = parseRpcResult(http_code, response_data); + if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size()); + return result; + } catch (...) { + if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size()); + throw; + } +} + json RPCClient::call(const std::string& method, const json& params, long timeoutSec) { std::lock_guard lk(curl_mutex_); diff --git a/src/rpc/rpc_client.h b/src/rpc/rpc_client.h index e5d588d..a1335fc 100644 --- a/src/rpc/rpc_client.h +++ b/src/rpc/rpc_client.h @@ -133,6 +133,17 @@ public: */ json call(const std::string& method, const json& params = json::array()); + /** + * @brief Like call(), but zeroes the raw HTTP response body after parsing (B7). + * + * For RPCs whose response carries a secret (z_exportmnemonic / z_exportkey), the parsed + * value is scrubbed by the caller — but the raw response string this method builds also + * holds that secret and is otherwise freed without zeroing. Use this variant so the largest, + * longest-lived plaintext copy doesn't linger in freed heap. Slightly slower (an extra wipe); + * only worth it for secret-bearing calls. + */ + json callSecret(const std::string& method, const json& params = json::array()); + /** * @brief Make a raw RPC call with a custom timeout * @param method RPC method name diff --git a/src/ui/windows/export_all_keys_dialog.cpp b/src/ui/windows/export_all_keys_dialog.cpp index 1821bb8..254f335 100644 --- a/src/ui/windows/export_all_keys_dialog.cpp +++ b/src/ui/windows/export_all_keys_dialog.cpp @@ -180,7 +180,7 @@ void ExportAllKeysDialog::render(App* app) for (const auto& addr : z_addrs) { try { rpc::RPCClient::TraceScope trace("Settings / Export all keys"); - auto result = rpc->call("z_exportkey", {addr}); + auto result = rpc->callSecret("z_exportkey", {addr}); // zero the raw body too (B7) if (result.is_string()) { keys += "# Address: " + addr + "\n"; keys += result.get() + "\n\n"; @@ -196,7 +196,7 @@ void ExportAllKeysDialog::render(App* app) for (const auto& addr : t_addrs) { try { rpc::RPCClient::TraceScope trace("Settings / Export all keys"); - auto result = rpc->call("dumpprivkey", {addr}); + auto result = rpc->callSecret("dumpprivkey", {addr}); // zero the raw body too (B7) if (result.is_string()) { keys += "# Address: " + addr + "\n"; keys += result.get() + "\n\n";