feat(wallets): recursive subdir scan + wallet.dat encryption/seed badges
Search user-added wallet folders RECURSIVELY (bounded: depth/hit/visit caps, skip_permission_denied, no symlink-follow, node-junk + node-subtree pruning); the datadir stays top-level. r.dir is each file's real parent so Open/import resolve subdir wallets. Auto-suffix the import destination on a name collision. Add util/wallet_file_probe.h: read encryption/seed/shielded flags straight off a wallet.dat WITHOUT loading it — validate the Berkeley DB btree magic, then a bounded streaming byte-scan for the length-prefixed record markers the daemon writes (mkey -> encrypted; hdseed/hdchain -> seed/HD; zkey/sapzkey -> shielded). Reads no key material. The Wallets dialog shows Encrypted / Seed / Legacy badges, plus an Unknown badge when a large file couldn't be fully scanned (so absence of a lock never falsely reads as unencrypted). Unit-tested + validated read-only against real wallets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
#include "util/pool_registry.h"
|
||||
#include "util/daemon_updater.h"
|
||||
#include "util/lite_server_probe.h"
|
||||
#include "util/wallet_file_probe.h"
|
||||
#include "wallet/lite_connection_service.h"
|
||||
#include "wallet/lite_diagnostics.h"
|
||||
#include "wallet/lite_owned_string.h"
|
||||
@@ -737,6 +738,60 @@ void testPaymentUri()
|
||||
EXPECT_EQ(invalid.error, std::string("Invalid negative amount"));
|
||||
}
|
||||
|
||||
void testWalletFileProbe()
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
using dragonx::util::probeWalletFile;
|
||||
const fs::path dir = fs::path("/tmp/dragonx") / "probe";
|
||||
std::error_code ec; fs::create_directories(dir, ec);
|
||||
auto writef = [](const fs::path& p, const std::string& d) {
|
||||
std::ofstream o(p, std::ios::binary); o.write(d.data(), (std::streamsize)d.size());
|
||||
};
|
||||
// A valid Berkeley DB btree metapage has the magic 0x00053162 at byte offset 12 (little-endian here).
|
||||
auto bdb = []() { std::string h(16, '\0'); h[12] = 0x62; h[13] = 0x31; h[14] = 0x05; h[15] = 0x00; return h; };
|
||||
// Wallet records are keyed by a CompactSize-length-prefixed name, e.g. "mkey" -> {0x04,'m','k','e','y'}.
|
||||
auto rec = [](const std::string& name) { return std::string(1, (char)name.size()) + name; };
|
||||
|
||||
// 1) Non-BDB file → not recognized, no flags.
|
||||
writef(dir / "junk.dat", std::string(4096, 'x'));
|
||||
{ auto p = probeWalletFile((dir / "junk.dat").string());
|
||||
EXPECT_FALSE(p.isBerkeleyDB); EXPECT_FALSE(p.encrypted); EXPECT_FALSE(p.hdSeed); }
|
||||
|
||||
// 2) BDB with no markers → legacy plaintext; a complete scan makes the negatives trustworthy.
|
||||
writef(dir / "legacy.dat", bdb() + std::string(2000, '\0'));
|
||||
{ auto p = probeWalletFile((dir / "legacy.dat").string());
|
||||
EXPECT_TRUE(p.isBerkeleyDB); EXPECT_FALSE(p.encrypted); EXPECT_FALSE(p.hdSeed);
|
||||
EXPECT_FALSE(p.hasShielded); EXPECT_TRUE(p.scanComplete); }
|
||||
|
||||
// 3) Encrypted (mkey) + HD (hdchain) + shielded (csapzkey).
|
||||
writef(dir / "enc.dat", bdb() + std::string(500, 'a') + rec("mkey") + std::string(300, 'b') +
|
||||
rec("hdchain") + std::string(300, 'c') + rec("csapzkey") + std::string(500, 'd'));
|
||||
{ auto p = probeWalletFile((dir / "enc.dat").string());
|
||||
EXPECT_TRUE(p.encrypted); EXPECT_TRUE(p.hdSeed); EXPECT_TRUE(p.hasShielded); }
|
||||
|
||||
// 4) False-positive guard: the bare word "mkey" WITHOUT the 0x04 length prefix must not count.
|
||||
writef(dir / "bare.dat", bdb() + std::string(100, '\0') + "mkey" + std::string(100, '\0'));
|
||||
{ auto p = probeWalletFile((dir / "bare.dat").string()); EXPECT_FALSE(p.encrypted); }
|
||||
|
||||
// 5) Byte cap: a marker beyond maxBytes is missed AND the scan is flagged incomplete (so a caller
|
||||
// won't wrongly assert "legacy"); the same file scanned fully finds it and reports complete.
|
||||
{ std::string d = bdb(); d.resize(2u * 1024 * 1024, 'q'); d += rec("mkey"); writef(dir / "deep.dat", d);
|
||||
auto capped = probeWalletFile((dir / "deep.dat").string(), 1024u * 1024u);
|
||||
EXPECT_TRUE(capped.isBerkeleyDB); EXPECT_FALSE(capped.encrypted); EXPECT_FALSE(capped.scanComplete);
|
||||
EXPECT_TRUE(capped.bytesRead <= 1024u * 1024u); // budget accounting reflects a truncated read
|
||||
auto full = probeWalletFile((dir / "deep.dat").string());
|
||||
EXPECT_TRUE(full.encrypted); EXPECT_TRUE(full.scanComplete); }
|
||||
|
||||
// 6) Exact-boundary case: a file whose length is an exact 1 MiB multiple (and == maxBytes) must still
|
||||
// report scanComplete=true (the peek() guard), so a fully-scanned wallet isn't mislabeled truncated.
|
||||
{ std::string d = bdb(); d.resize(1024u * 1024u, 'e'); // exactly one 1 MiB chunk, no markers
|
||||
writef(dir / "exact.dat", d);
|
||||
auto p = probeWalletFile((dir / "exact.dat").string(), 1024u * 1024u);
|
||||
EXPECT_TRUE(p.isBerkeleyDB); EXPECT_TRUE(p.scanComplete); EXPECT_FALSE(p.encrypted); EXPECT_FALSE(p.hdSeed); }
|
||||
|
||||
fs::remove_all(dir, ec);
|
||||
}
|
||||
|
||||
void testAmountFormatting()
|
||||
{
|
||||
EXPECT_EQ(dragonx::util::formatAmountFixed(1.0), std::string("1.00000000"));
|
||||
@@ -6263,6 +6318,7 @@ int main()
|
||||
testHushChatOutgoing();
|
||||
testHushChatTransport();
|
||||
testHushChatShuffledReceive();
|
||||
testWalletFileProbe();
|
||||
testAddressChecksumValidation();
|
||||
testLiteServerProbeLive();
|
||||
testXmrigLiveInstall();
|
||||
|
||||
Reference in New Issue
Block a user