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>
155 lines
6.4 KiB
C++
155 lines
6.4 KiB
C++
#include "daemon/seed_wallet_creator.h"
|
|
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <thread>
|
|
|
|
#include <sodium.h>
|
|
|
|
#include "daemon/embedded_daemon.h"
|
|
#include "rpc/rpc_client.h"
|
|
#include "util/platform.h"
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace dragonx {
|
|
namespace daemon {
|
|
|
|
namespace {
|
|
|
|
// Random alphanumeric token for the isolated node's throwaway RPC credentials (libsodium CSPRNG;
|
|
// sodium_init() has already run at app startup for the chat crypto).
|
|
std::string randomToken(int n)
|
|
{
|
|
static const char cs[] =
|
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
std::string s;
|
|
s.reserve(n);
|
|
for (int i = 0; i < n; ++i)
|
|
s.push_back(cs[randombytes_uniform(sizeof(cs) - 1)]);
|
|
return s;
|
|
}
|
|
|
|
// A free localhost port for the isolated node — just above the default so it never collides with
|
|
// the main daemon (which keeps running on the default port throughout).
|
|
int pickFreePort()
|
|
{
|
|
for (int p = 21770; p < 21900; ++p)
|
|
if (!EmbeddedDaemon::tcpPortInUse(p))
|
|
return p;
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
|
|
const std::function<void(const std::string&)>& progress)
|
|
{
|
|
auto report = [&](const std::string& m) { if (progress) progress(m); };
|
|
SeedWalletResult r;
|
|
std::error_code ec;
|
|
|
|
// 1. Isolated throwaway datadir. The Komodo/Hush daemon requires the datadir's basename to be
|
|
// the assetchain name (DRAGONX) — mirroring ~/.hush/DRAGONX — or it mis-resolves its conf and
|
|
// RPC port. So the wallet lives in <base>/DRAGONX; `base` is the migration root we clean up.
|
|
const std::string base = util::Platform::getConfigDir() + "/seed-migrate";
|
|
const std::string dataDir = base + "/DRAGONX";
|
|
fs::remove_all(base, ec);
|
|
fs::create_directories(dataDir, ec);
|
|
if (ec) { r.error = "Could not create the temporary wallet directory."; return r; }
|
|
|
|
// 2. Free port + fresh throwaway RPC credentials for the isolated node.
|
|
const int port = pickFreePort();
|
|
if (port <= 0) { r.error = "No free local port for the isolated node."; return r; }
|
|
const std::string user = randomToken(16);
|
|
const std::string pass = randomToken(32);
|
|
|
|
// 3. Minimal conf for the isolated node (own creds/port). The DRAGONX RPC is plaintext HTTP on
|
|
// localhost — `-tls=only` applies to P2P, not the RPC — so the client below connects without
|
|
// TLS, exactly as the main GUI does (its conf has no rpctls key either).
|
|
const std::string conf = "rpcuser=" + user + "\n"
|
|
"rpcpassword=" + pass + "\n"
|
|
"rpcport=" + std::to_string(port) + "\n"
|
|
"server=1\n";
|
|
if (!util::Platform::writeFileAtomically(dataDir + "/DRAGONX.conf", conf,
|
|
/*restrictPermissions=*/true)) {
|
|
r.error = "Could not write the isolated node config.";
|
|
fs::remove_all(base, ec);
|
|
return r;
|
|
}
|
|
|
|
// 4. Start the isolated daemon: fresh mnemonic wallet (-usemnemonic=1), no network/sync.
|
|
report("Starting an isolated node…");
|
|
EmbeddedDaemon temp;
|
|
temp.setSkipPortCheck(true); // runs on `port`, beside the main daemon on the default port
|
|
temp.setNextStartOverride(dataDir, {"-usemnemonic=1", "-connect=0", "-listen=0",
|
|
"-maxconnections=0"});
|
|
if (!temp.start("")) {
|
|
r.error = "Could not start the isolated node: " + temp.getLastError();
|
|
fs::remove_all(base, ec);
|
|
return r;
|
|
}
|
|
|
|
// 5. Connect to it, retrying until the RPC is responsive and past warmup.
|
|
report("Creating your new seed wallet…");
|
|
rpc::RPCClient cli;
|
|
bool ready = false;
|
|
for (int i = 0; i < 90 && !ready; ++i) {
|
|
if (cli.connect("127.0.0.1", std::to_string(port), user, pass, /*useTls=*/false)) {
|
|
try { cli.call("getinfo"); ready = true; } // succeeds only once past warmup (-28)
|
|
catch (...) { cli.disconnect(); }
|
|
}
|
|
if (!ready) std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
}
|
|
if (!ready) {
|
|
r.error = "The isolated node did not become ready in time.";
|
|
temp.stop(20000);
|
|
fs::remove_all(base, ec);
|
|
return r;
|
|
}
|
|
|
|
// 6. Export the new seed phrase + a fresh shielded receive address (the future sweep target).
|
|
try {
|
|
auto m = cli.callSecret("z_exportmnemonic"); // zero the raw body too (B7)
|
|
if (m.contains("mnemonic") && m["mnemonic"].is_string()) {
|
|
// Take our copy, then scrub the json node's own copy so it isn't freed in the clear (B7).
|
|
auto& mn = m["mnemonic"].get_ref<std::string&>();
|
|
r.seedPhrase = mn;
|
|
if (!mn.empty()) sodium_memzero(&mn[0], mn.size());
|
|
}
|
|
r.destAddress = cli.call("z_getnewaddress").get<std::string>();
|
|
r.ok = !r.seedPhrase.empty() && !r.destAddress.empty();
|
|
if (!r.ok) r.error = "The isolated node returned an empty seed or address.";
|
|
} catch (const std::exception& e) {
|
|
const std::string what = e.what();
|
|
// "Method not found" (JSON-RPC -32601) means this dragonxd predates mnemonic support —
|
|
// it has no z_exportmnemonic RPC (the older bundled binary). Migrate-to-seed can't work
|
|
// until the daemon is updated, so give an actionable message, not the raw RPC error.
|
|
if (what.find("Method not found") != std::string::npos ||
|
|
what.find("-32601") != std::string::npos) {
|
|
r.error = "This DragonX daemon is too old to create a seed wallet — it lacks mnemonic "
|
|
"support (the z_exportmnemonic RPC). Update to the latest DragonX daemon "
|
|
"(Settings -> NODE & SECURITY -> Check for updates, or Install bundled), then "
|
|
"try again.";
|
|
} else {
|
|
r.error = std::string("Seed export failed: ") + what;
|
|
}
|
|
}
|
|
|
|
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
|
|
cli.disconnect();
|
|
temp.stop(20000);
|
|
|
|
// 8. Keep the temp wallet for a later sweep/adopt step, or scrub it. tempDatadir is the
|
|
// migration root `base`; the new wallet.dat lives in <base>/DRAGONX.
|
|
r.tempDatadir = base;
|
|
if (!keepDatadir || !r.ok) {
|
|
fs::remove_all(base, ec);
|
|
r.tempDatadir.clear();
|
|
}
|
|
return r;
|
|
}
|
|
|
|
} // namespace daemon
|
|
} // namespace dragonx
|