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
+4 -13
View File
@@ -240,22 +240,13 @@ private:
// daemon off the main thread (to avoid stalling the UI), or ask the user to
// restart an external daemon. Shared by encryptWalletWithPassphrase() and
// processDeferredEncryption(); must be called on the main thread.
// Zero (overwrite) then delete a plaintext key export so a full cleartext dump of every private key is
// never left readable on disk. Idempotent + error-tolerant (safe on a missing/locked file). (H-02)
// Best-effort secure delete (overwrite in place, fsync, unlink) of a plaintext key export, so a full
// cleartext dump of every private key is not left readable on disk. Idempotent + error-tolerant
// (safe on a missing/locked file). (H-02)
void App::scrubAndRemoveExport(const std::string& path)
{
if (path.empty()) return;
std::error_code ec;
const auto sz = std::filesystem::file_size(path, ec);
if (!ec && sz > 0) {
std::fstream scrub(path, std::ios::binary | std::ios::in | std::ios::out);
if (scrub) {
const std::vector<char> zeros(static_cast<size_t>(sz), 0);
scrub.write(zeros.data(), static_cast<std::streamsize>(sz));
scrub.flush();
}
}
std::filesystem::remove(path, ec);
util::Platform::secureWipeAndRemove(path);
}
// Startup net for H-02: a crash/kill/early-return between exporting the cleartext keys and scrubbing them