feat(rpc): scrub the raw response body for secret-bearing calls (B7)

The secret exports (z_exportmnemonic / z_exportkey / dumpprivkey) already scrub
the parsed value at the call site, but the raw HTTP response string those RPCs
build — the curl write buffer, which holds the same secret in the clear — was
freed without zeroing. That's the "fuller fix belongs in the RPC layer" the
identity-fetch comment flagged.

Add RPCClient::callSecret(), a call() variant that sodium_memzeros the raw
response body after parsing (on success and on throw). NRVO makes the returned
string the very buffer curl wrote into, so one wipe covers it. Route every
secret-bearing export through it: chat identity (mnemonic + z_exportkey
fallback), Settings seed-phrase + single-key export, Export-all-keys, and the
migrate-to-seed isolated-node mnemonic export. Purely additive — the parsed
result is byte-identical, so no behavior change (safe for the fund-critical
migrate path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 16:45:41 -05:00
parent bce69362eb
commit fecc9015fb
5 changed files with 39 additions and 7 deletions

View File

@@ -2632,7 +2632,7 @@ void App::exportPrivateKey(const std::string& address, std::function<void(const
std::string err;
try {
rpc::RPCClient::TraceScope trace("Settings / Export private key");
key = rpc_->call(method, {address}).get<std::string>();
key = rpc_->callSecret(method, {address}).get<std::string>(); // 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<std::string&>();
secret = key;
@@ -3374,7 +3374,7 @@ void App::exportSeedPhrase(std::function<void(bool, bool, const std::string&, co
bool noMnemonic = false;
rpc::RPCClient::TraceScope trace("Settings / Export seed phrase");
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()) {
auto& m = response["mnemonic"].get_ref<std::string&>();
phrase = m;