fix(security): overwrite key/PIN files with fsync before deleting them

Two secure-delete paths overwrote a sensitive file and then unlinked it,
but neither reliably forced the overwrite to stable storage. The
plaintext key export used a buffered stream with only a flush, and the
PIN vault removal opened a second descriptor to fsync only on non-
Windows. On a crash, or on Windows, the zeroing write could stay in the
OS cache and the original bytes could survive on disk.

Both now go through one shared best-effort secure-delete helper:

- It overwrites the file in place in chunks, never truncating first, so
  the original blocks are the ones rewritten.
- It flushes the data to the device before unlinking (full fsync on
  macOS, write-through on Windows), then removes the file.
- It is safe on a missing file and reports whether the file is gone.

This is still best-effort: SSD wear-levelling and copy-on-write or
journaling filesystems may retain copies.

Tests exercise the overwrite-and-remove helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 19:24:12 -05:00
co-authored by Claude Opus 4.8
parent 63a422a615
commit 2a535c844d
5 changed files with 110 additions and 44 deletions
+41
View File
@@ -815,6 +815,46 @@ void testClassifyRpcFailure()
EXPECT_FALSE(dragonx::rpc::isRpcTimeout(K::QueueFull));
}
void testSecureWipeAndRemove()
{
namespace fs = std::filesystem;
using dragonx::util::Platform;
std::error_code ec;
const fs::path dir = fs::temp_directory_path(ec) / "od_secwipe_test";
fs::create_directories(dir, ec);
const std::string f = (dir / "victim.bin").string();
{
std::ofstream o(f, std::ios::binary);
const std::string blob(200 * 1024, static_cast<char>(0xAB));
o.write(blob.data(), static_cast<std::streamsize>(blob.size()));
}
EXPECT_TRUE(fs::exists(f, ec));
{
std::ifstream hold(f, std::ios::binary); // second handle open across the wipe
EXPECT_TRUE(Platform::secureWipeAndRemove(f));
}
EXPECT_FALSE(fs::exists(f, ec));
EXPECT_TRUE(Platform::secureWipeAndRemove(f)); // missing path
#ifndef _WIN32
const std::string target = (dir / "target.bin").string();
const std::string link = (dir / "link.bin").string();
const std::string payload(4096, 'Z');
{
std::ofstream o(target, std::ios::binary);
o.write(payload.data(), static_cast<std::streamsize>(payload.size()));
}
fs::create_symlink(target, link, ec);
if (!ec) {
EXPECT_TRUE(Platform::secureWipeAndRemove(link));
EXPECT_FALSE(fs::is_symlink(link, ec));
std::ifstream in(target, std::ios::binary);
std::string got((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
EXPECT_TRUE(got == payload); // target untouched
}
#endif
fs::remove_all(dir, ec);
}
void testClassifyUnlockFailure()
{
using dragonx::rpc::RpcError;
@@ -18213,6 +18253,7 @@ int main()
testConnectionConfig();
testClassifyRpcFailure();
testClassifyUnlockFailure();
testSecureWipeAndRemove();
testFormatHttpError();
testRpcFailureKindOf();
testPaymentUri();