fix(chat,rpc): address adversarial review of the chat backlog
Four confirmed findings from the review pass: SECURITY — B7 was incomplete: - The single-key Export dialog (key_export_dialog) still used plain call() for z_exportkey/dumpprivkey/z_exportviewingkey — a live spending-key leak on the most common per-address export path, missed by the B7 commit. - callSecret() zeros the raw body but the parsed json holds its OWN heap copy of the secret; several callers did .get<string>() on a temporary json and freed that copy un-wiped. Fix: add RPCClient::callSecretString() — returns the bare-string result with BOTH the raw body AND the json node zeroed, so callers can't forget. Route key_export_dialog (×2), exportPrivateKey, and export_all_keys (×2) through it; scrub the z_exportmnemonic json node in seed_wallet_creator (object result); also wipe the transient key copies, the displayed s_key on reset, and the aggregated export-all `keys` buffer. CHAT: - Jump-to-latest pill: SetCursorScreenPos moved the parent cursor and never restored it, so the composer footer rendered ~8px too high while scrolled up. Save + restore the cursor around the pill. - New-message toast: gating on a chatUnreadCount() watermark delta could be swallowed when an outgoing echo (wall-clock) pushed the seen-watermark past a later reply's block time. ingest() now reports the cids it appended; the toast fires when any is a non-muted conversation — skew-proof, still mute-aware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -391,6 +391,35 @@ json RPCClient::callSecret(const std::string& method, const json& params)
|
||||
}
|
||||
}
|
||||
|
||||
std::string RPCClient::callSecretString(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");
|
||||
}
|
||||
|
||||
long http_code = 0;
|
||||
std::string response_data = performCall(method, params, http_code);
|
||||
try {
|
||||
json result = parseRpcResult(http_code, response_data);
|
||||
std::string out;
|
||||
if (result.is_string()) {
|
||||
// Copy the secret out, then zero the json node's OWN heap buffer — parseRpcResult's
|
||||
// json::parse allocates this independently of response_data, so scrubbing the raw body
|
||||
// alone would leave it behind (B7). `out` is now the only live copy; the caller wipes it.
|
||||
auto& s = result.get_ref<std::string&>();
|
||||
out = s;
|
||||
if (!s.empty()) sodium_memzero(&s[0], s.size());
|
||||
}
|
||||
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
||||
if (!result.is_string()) throw std::runtime_error("RPC result is not a string");
|
||||
return out;
|
||||
} 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_);
|
||||
|
||||
@@ -144,6 +144,15 @@ public:
|
||||
*/
|
||||
json callSecret(const std::string& method, const json& params = json::array());
|
||||
|
||||
/**
|
||||
* @brief callSecret() for RPCs whose result is a bare secret string (z_exportkey / dumpprivkey /
|
||||
* z_exportviewingkey). Returns the result string with BOTH the raw response body AND the
|
||||
* parsed json's own copy of the secret zeroed — so no un-scrubbed heap copy survives the
|
||||
* call. The returned std::string is the only remaining copy; the caller owns it and must
|
||||
* wipe it (sodium_memzero) when done. Throws if the result is not a JSON string.
|
||||
*/
|
||||
std::string callSecretString(const std::string& method, const json& params = json::array());
|
||||
|
||||
/**
|
||||
* @brief Make a raw RPC call with a custom timeout
|
||||
* @param method RPC method name
|
||||
|
||||
Reference in New Issue
Block a user