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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user