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:
2026-07-16 17:11:31 -05:00
co-authored by Claude Opus 4.8
parent 9cb91415d9
commit fac0245297
9 changed files with 99 additions and 38 deletions
+9
View File
@@ -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