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
+52
View File
@@ -300,6 +300,58 @@ bool Platform::deleteFile(const std::string& path)
#endif
}
bool Platform::secureWipeAndRemove(const std::string& path)
{
// Best-effort; SSD wear-levelling and CoW/journaling filesystems may retain copies.
if (path.empty()) return false;
static constexpr size_t kChunk = 64 * 1024;
const std::string buf(kChunk, static_cast<char>(0xAB));
#ifdef _WIN32
HANDLE h = CreateFileA(path.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, nullptr);
if (h != INVALID_HANDLE_VALUE) {
LARGE_INTEGER sz;
if (GetFileSizeEx(h, &sz) && sz.QuadPart > 0) {
long long left = sz.QuadPart;
while (left > 0) {
DWORD n = static_cast<DWORD>(left < (long long)kChunk ? left : (long long)kChunk);
DWORD written = 0;
if (!WriteFile(h, buf.data(), n, &written, nullptr) || written == 0) break;
left -= written;
}
FlushFileBuffers(h);
}
CloseHandle(h);
}
DeleteFileA(path.c_str());
return GetFileAttributesA(path.c_str()) == INVALID_FILE_ATTRIBUTES;
#else
int fd = ::open(path.c_str(), O_WRONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd >= 0) {
struct stat st;
if (::fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0) {
off_t off = 0;
const off_t total = st.st_size;
while (off < total) {
size_t n = static_cast<size_t>(total - off < (off_t)kChunk ? total - off : (off_t)kChunk);
ssize_t w = ::pwrite(fd, buf.data(), n, off);
if (w <= 0) break;
off += w;
}
#ifdef __APPLE__
if (::fcntl(fd, F_FULLFSYNC) != 0) ::fsync(fd);
#else
::fsync(fd);
#endif
}
::close(fd);
}
::remove(path.c_str());
struct stat st2;
return ::lstat(path.c_str(), &st2) != 0 && errno == ENOENT;
#endif
}
bool Platform::writeFileAtomically(const std::string& path,
const std::string& content,
bool restrictPermissions)