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

@@ -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<std::recursive_mutex> 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<std::recursive_mutex> lk(curl_mutex_);

View File

@@ -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