From 11b50738fe4339b524296ea55d12f6c812f8a5c2 Mon Sep 17 00:00:00 2001 From: hairetikos <19870044+hairetikos@users.noreply.github.com> Date: Tue, 25 Nov 2025 03:17:58 +0000 Subject: [PATCH 1/4] disable debug.log by default this insecure default was inherited from zcash debug.log contains a lot of sensitive transaction metadata, it should only be enabled for debugging purposes (hence, it is called debug.log) disable it by default --- src/util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.cpp b/src/util.cpp index 26486d25c92..2ea5d583cc9 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -106,7 +106,7 @@ map mapArgs; map > mapMultiArgs; bool fDebug = false; bool fPrintToConsole = false; -bool fPrintToDebugLog = true; +bool fPrintToDebugLog = false; bool fDaemon = false; bool fServer = false; string strMiscWarning; From 8fbb67b70967372f693f34836c559c4ede801866 Mon Sep 17 00:00:00 2001 From: hairetikos <19870044+hairetikos@users.noreply.github.com> Date: Tue, 25 Nov 2025 03:20:33 +0000 Subject: [PATCH 2/4] Add `debuglogfile` conf option since we now disable it by default --- src/init.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/init.cpp b/src/init.cpp index f849b868ec4..d18c8fd09cc 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -474,6 +474,7 @@ std::string HelpMessage(HelpMessageMode mode) strUsage += HelpMessageOpt("-help-debug", _("Show all debugging options (usage: --help -help-debug)")); strUsage += HelpMessageOpt("-logips", strprintf(_("Include IP addresses in debug output (default: %u)"), 0)); strUsage += HelpMessageOpt("-logtimestamps", strprintf(_("Prepend debug output with timestamp (default: %u)"), 1)); + strUsage += HelpMessageOpt("-debuglogfile", _("Write debug output to debug.log file (default: 0, disabled for privacy)")); if (showDebug) { strUsage += HelpMessageOpt("-limitfreerelay=", strprintf("Continuously rate-limit free transactions to *1000 bytes per minute (default: %u)", 15)); @@ -14486,6 +14487,7 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) // Set this early so that parameter interactions go to console fPrintToConsole = GetBoolArg("-printtoconsole", false); fLogTimestamps = GetBoolArg("-logtimestamps", true); + fPrintToDebugLog = GetBoolArg("-debuglogfile", false); fLogIPs = GetBoolArg("-logips", false); LogPrintf("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); From b7e6395cdb136f0093d71e1036c753b2d86c7647 Mon Sep 17 00:00:00 2001 From: hairetikos <19870044+hairetikos@users.noreply.github.com> Date: Tue, 25 Nov 2025 03:29:07 +0000 Subject: [PATCH 3/4] implement secure shredding for `debug.log` & onion V3 key These functions securely shred files using a 3-pass overwrite pattern, ensuring sensitive data is irrecoverable. This is important because the `debug.log` file may contain sensitive transaction metadata. `debug.log` should only be used for debugging purposes. a function to also securely shred the onion V3 private key. upon restarting the node a new V3 key and address will be auto-generated --- src/rpc/misc.cpp | 272 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index f6d31c1641f..17ff25d6086 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -20,6 +20,10 @@ #include #include +#include + +#include +#include #include @@ -483,6 +487,270 @@ UniValue setmocktime(const UniValue& params, bool fHelp) return NullUniValue; } +/** + * Securely shred a file using DoD 5220.22-M style overwrite pattern + * + * SECURITY: This function performs a secure wipe by: + * 1. Overwriting the entire file with 0xFF (all 1s) + * 2. Overwriting the entire file with 0xAA (10101010 pattern) + * 3. Overwriting the entire file with 0x00 (all 0s) + * 4. Flushing to disk after each pass + * 5. Renaming to obscure original filename + * 6. Deleting the file + * + * @param filepath The path to the file to securely destroy + * @param progressCallback Optional callback for progress updates (0-100) + * @return true if successful, false otherwise + */ +static bool SecureShredFile(const boost::filesystem::path& filepath, + std::function progressCallback = nullptr) +{ + if (!boost::filesystem::exists(filepath)) { + return false; + } + + try { + // Get file size + uintmax_t fileSize = boost::filesystem::file_size(filepath); + if (fileSize == 0) { + // Empty file, just delete it + boost::filesystem::remove(filepath); + return true; + } + + // Allocate buffer for overwriting (use 64KB chunks for efficiency) + const size_t BUFFER_SIZE = 65536; + std::vector buffer(BUFFER_SIZE); + + // Three-pass overwrite pattern (DoD 5220.22-M inspired) + const unsigned char patterns[3] = { + 0xFF, // Pass 1: All 1s (11111111) + 0xAA, // Pass 2: Alternating (10101010) + 0x00 // Pass 3: All 0s (00000000) + }; + + // Total work = 3 passes * fileSize + uintmax_t totalWork = fileSize * 3; + uintmax_t completedWork = 0; + + for (int pass = 0; pass < 3; pass++) { + // Fill buffer with current pattern + std::memset(&buffer[0], patterns[pass], BUFFER_SIZE); + + // Open file for binary writing + std::ofstream file(filepath.string().c_str(), + std::ios::binary | std::ios::in | std::ios::out); + if (!file.is_open()) { + return false; + } + + // Overwrite entire file + uintmax_t remaining = fileSize; + while (remaining > 0) { + size_t toWrite = (remaining > BUFFER_SIZE) ? BUFFER_SIZE : static_cast(remaining); + file.write(reinterpret_cast(&buffer[0]), toWrite); + if (!file.good()) { + file.close(); + return false; + } + remaining -= toWrite; + completedWork += toWrite; + + // Report progress + if (progressCallback) { + int percent = static_cast((completedWork * 100) / totalWork); + progressCallback(percent); + } + } + + // Flush to ensure data is written to disk + file.flush(); + file.close(); + + // Force sync to disk (platform-specific) + FILE* fp = fopen(filepath.string().c_str(), "rb"); + if (fp) { +#ifdef WIN32 + _commit(_fileno(fp)); +#else + fsync(fileno(fp)); +#endif + fclose(fp); + } + } + + // Rename to obscure original filename before deletion + boost::filesystem::path obscuredPath = filepath.parent_path() / "00000000000000000000000000000000"; + + // Handle case where obscured name already exists + int counter = 0; + boost::filesystem::path finalObscuredPath = obscuredPath; + while (boost::filesystem::exists(finalObscuredPath)) { + finalObscuredPath = filepath.parent_path() / + ("00000000000000000000000000000000_" + std::to_string(counter++)); + } + + boost::filesystem::rename(filepath, finalObscuredPath); + + // Delete the file + boost::filesystem::remove(finalObscuredPath); + + return true; + + } catch (const boost::filesystem::filesystem_error& e) { + return false; + } catch (const std::exception& e) { + return false; + } +} + +UniValue shredlogs(const UniValue& params, bool fHelp) +{ + if (fHelp || params.size() != 0) + throw runtime_error( + "shredlogs\n" + "\nSecurely destroy debug.log and db.log files in the data directory.\n" + "\nThis command performs a secure 3-pass overwrite before deletion:\n" + " Pass 1: Overwrite with 0xFF (all 1s)\n" + " Pass 2: Overwrite with 0xAA (10101010 pattern)\n" + " Pass 3: Overwrite with 0x00 (all 0s)\n" + "\nAfter overwriting, files are renamed to obscure the original filename,\n" + "then deleted. Shredding is important because the debug.log file may contain \n" + "sensitive transaction metadata, it should ONLY be used for debugging.\n" + "\nWARNING: This operation is irreversible!\n" + "\nResult:\n" + "{\n" + " \"debug.log\": { \"status\": \"shredded\"|\"not found\"|\"failed\", \"size\": n, \"progress\": 100 },\n" + " \"db.log\": { \"status\": \"shredded\"|\"not found\"|\"failed\", \"size\": n, \"progress\": 100 }\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("shredlogs", "") + + HelpExampleRpc("shredlogs", "") + ); + + UniValue result(UniValue::VOBJ); + boost::filesystem::path dataDir = GetDataDir(); + + // Temporarily disable debug log file writing + bool wasLoggingToFile = fPrintToDebugLog; + fPrintToDebugLog = false; + + // Shred debug.log + boost::filesystem::path debugLogPath = dataDir / "debug.log"; + UniValue debugResult(UniValue::VOBJ); + + if (boost::filesystem::exists(debugLogPath)) { + uintmax_t fileSize = boost::filesystem::file_size(debugLogPath); + debugResult.push_back(Pair("size", (int64_t)fileSize)); + + int lastProgress = -1; + auto progressCb = [&lastProgress, &debugResult](int progress) { + lastProgress = progress; + }; + + if (SecureShredFile(debugLogPath, progressCb)) { + debugResult.push_back(Pair("status", "shredded")); + debugResult.push_back(Pair("progress", 100)); + } else { + debugResult.push_back(Pair("status", "failed")); + debugResult.push_back(Pair("progress", lastProgress)); + } + } else { + debugResult.push_back(Pair("status", "not found")); + debugResult.push_back(Pair("size", 0)); + debugResult.push_back(Pair("progress", 0)); + } + result.push_back(Pair("debug.log", debugResult)); + + // Shred db.log + boost::filesystem::path dbLogPath = dataDir / "db.log"; + UniValue dbResult(UniValue::VOBJ); + + if (boost::filesystem::exists(dbLogPath)) { + uintmax_t fileSize = boost::filesystem::file_size(dbLogPath); + dbResult.push_back(Pair("size", (int64_t)fileSize)); + + int lastProgress = -1; + auto progressCb = [&lastProgress](int progress) { + lastProgress = progress; + }; + + if (SecureShredFile(dbLogPath, progressCb)) { + dbResult.push_back(Pair("status", "shredded")); + dbResult.push_back(Pair("progress", 100)); + } else { + dbResult.push_back(Pair("status", "failed")); + dbResult.push_back(Pair("progress", lastProgress)); + } + } else { + dbResult.push_back(Pair("status", "not found")); + dbResult.push_back(Pair("size", 0)); + dbResult.push_back(Pair("progress", 0)); + } + result.push_back(Pair("db.log", dbResult)); + + // DO NOT re-enable logging - keep it disabled so no new debug.log is created + + return result; +} + +UniValue shredonion(const UniValue& params, bool fHelp) +{ + if (fHelp || params.size() != 0) + throw runtime_error( + "shredonion\n" + "\nSecurely destroy the Tor onion service private key file.\n" + "\nThis command securely wipes the 'onion_v3_private_key' file\n" + "in the data directory using a 3-pass overwrite pattern:\n" + " Pass 1: Overwrite with 0xFF (all 1s)\n" + " Pass 2: Overwrite with 0xAA (10101010 pattern)\n" + " Pass 3: Overwrite with 0x00 (all 0s)\n" + "\nAfter overwriting, the file is renamed to obscure the original\n" + "filename, then deleted.\n" + "\nWARNING: This operation is irreversible! Your node will generate\n" + "a new .onion address on next restart with Tor enabled.\n" + "\nResult:\n" + "{\n" + " \"onion_v3_private_key\": { \"status\": \"shredded\"|\"not found\"|\"failed\", \"size\": n, \"progress\": 100 }\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("shredonion", "") + + HelpExampleRpc("shredonion", "") + ); + + UniValue result(UniValue::VOBJ); + boost::filesystem::path dataDir = GetDataDir(); + boost::filesystem::path onionKeyPath = dataDir / "onion_v3_private_key"; + + UniValue onionResult(UniValue::VOBJ); + + if (boost::filesystem::exists(onionKeyPath)) { + uintmax_t fileSize = boost::filesystem::file_size(onionKeyPath); + onionResult.push_back(Pair("size", (int64_t)fileSize)); + + int lastProgress = -1; + auto progressCb = [&lastProgress](int progress) { + lastProgress = progress; + }; + + if (SecureShredFile(onionKeyPath, progressCb)) { + onionResult.push_back(Pair("status", "shredded")); + onionResult.push_back(Pair("progress", 100)); + } else { + onionResult.push_back(Pair("status", "failed")); + onionResult.push_back(Pair("progress", lastProgress)); + } + } else { + onionResult.push_back(Pair("status", "not found")); + onionResult.push_back(Pair("size", 0)); + onionResult.push_back(Pair("progress", 0)); + } + + result.push_back(Pair("onion_v3_private_key", onionResult)); + + return result; +} + static const CRPCCommand commands[] = { // category name actor (function) okSafeMode // --------------------- ------------------------ ----------------------- ---------- @@ -491,6 +759,10 @@ static const CRPCCommand commands[] = { "util", "z_validateaddress", &z_validateaddress, true }, /* uses wallet if enabled */ { "util", "createmultisig", &createmultisig, true }, { "util", "verifymessage", &verifymessage, true }, + + /* Privacy commands */ + { "privacy", "shredlogs", &shredlogs, true }, + { "privacy", "shredonion", &shredonion, true }, /* Not shown in help */ { "hidden", "setmocktime", &setmocktime, true }, From f7c221bd6f0dfa86095870f46a078cd331945839 Mon Sep 17 00:00:00 2001 From: hairetikos <19870044+hairetikos@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:26:56 +0000 Subject: [PATCH 4/4] fix face condition & file locking --- src/rpc/misc.cpp | 209 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 169 insertions(+), 40 deletions(-) diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index 17ff25d6086..e5dae838a79 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -24,9 +24,18 @@ #include #include +#include #include +#ifdef WIN32 +#include +#else +#include +#include +#include +#endif + #include "zcash/Address.hpp" using namespace std; @@ -505,79 +514,199 @@ UniValue setmocktime(const UniValue& params, bool fHelp) static bool SecureShredFile(const boost::filesystem::path& filepath, std::function progressCallback = nullptr) { - if (!boost::filesystem::exists(filepath)) { - return false; - } - try { - // Get file size - uintmax_t fileSize = boost::filesystem::file_size(filepath); - if (fileSize == 0) { - // Empty file, just delete it + // Open file immediately with read+write access to avoid TOCTOU race condition + // This also acquires an exclusive handle to the file +#ifdef WIN32 + // Windows: Open with exclusive access to lock the file + HANDLE hFile = CreateFileW( + filepath.wstring().c_str(), + GENERIC_READ | GENERIC_WRITE, + 0, // No sharing - exclusive access + NULL, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + + if (hFile == INVALID_HANDLE_VALUE) { + DWORD err = GetLastError(); + if (err == ERROR_FILE_NOT_FOUND) { + return false; // File doesn't exist + } + return false; // Could not open/lock file + } + + // Get file size using the open handle (avoids TOCTOU) + LARGE_INTEGER fileSize; + if (!GetFileSizeEx(hFile, &fileSize)) { + CloseHandle(hFile); + return false; + } + + uintmax_t size = static_cast(fileSize.QuadPart); + + if (size == 0) { + // Empty file, just close and delete it + CloseHandle(hFile); boost::filesystem::remove(filepath); return true; } - + // Allocate buffer for overwriting (use 64KB chunks for efficiency) const size_t BUFFER_SIZE = 65536; std::vector buffer(BUFFER_SIZE); - + // Three-pass overwrite pattern (DoD 5220.22-M inspired) const unsigned char patterns[3] = { 0xFF, // Pass 1: All 1s (11111111) 0xAA, // Pass 2: Alternating (10101010) 0x00 // Pass 3: All 0s (00000000) }; - + // Total work = 3 passes * fileSize - uintmax_t totalWork = fileSize * 3; + uintmax_t totalWork = size * 3; uintmax_t completedWork = 0; - + for (int pass = 0; pass < 3; pass++) { // Fill buffer with current pattern std::memset(&buffer[0], patterns[pass], BUFFER_SIZE); - - // Open file for binary writing - std::ofstream file(filepath.string().c_str(), - std::ios::binary | std::ios::in | std::ios::out); - if (!file.is_open()) { + + // Seek to beginning of file + LARGE_INTEGER zero; + zero.QuadPart = 0; + if (!SetFilePointerEx(hFile, zero, NULL, FILE_BEGIN)) { + CloseHandle(hFile); return false; } - + // Overwrite entire file - uintmax_t remaining = fileSize; + uintmax_t remaining = size; while (remaining > 0) { - size_t toWrite = (remaining > BUFFER_SIZE) ? BUFFER_SIZE : static_cast(remaining); - file.write(reinterpret_cast(&buffer[0]), toWrite); - if (!file.good()) { - file.close(); + DWORD toWrite = (remaining > BUFFER_SIZE) ? BUFFER_SIZE : static_cast(remaining); + DWORD bytesWritten; + if (!WriteFile(hFile, &buffer[0], toWrite, &bytesWritten, NULL) || bytesWritten != toWrite) { + CloseHandle(hFile); return false; } - remaining -= toWrite; - completedWork += toWrite; - + remaining -= bytesWritten; + completedWork += bytesWritten; + // Report progress if (progressCallback) { int percent = static_cast((completedWork * 100) / totalWork); progressCallback(percent); } } - - // Flush to ensure data is written to disk - file.flush(); - file.close(); - - // Force sync to disk (platform-specific) - FILE* fp = fopen(filepath.string().c_str(), "rb"); - if (fp) { -#ifdef WIN32 - _commit(_fileno(fp)); + + // Flush to disk using the SAME handle we wrote to + if (!FlushFileBuffers(hFile)) { + CloseHandle(hFile); + return false; + } + } + + // Close the file handle BEFORE rename/delete + CloseHandle(hFile); + hFile = INVALID_HANDLE_VALUE; + #else - fsync(fileno(fp)); -#endif - fclose(fp); + // POSIX: Open with exclusive lock + int fd = open(filepath.string().c_str(), O_RDWR); + if (fd < 0) { + if (errno == ENOENT) { + return false; // File doesn't exist + } + return false; // Could not open file + } + + // Acquire exclusive lock on the file + struct flock fl; + fl.l_type = F_WRLCK; // Exclusive write lock + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; // Lock entire file + + if (fcntl(fd, F_SETLK, &fl) < 0) { + // Could not acquire lock - file may be in use + close(fd); + return false; + } + + // Get file size using fstat on the open descriptor (avoids TOCTOU) + struct stat st; + if (fstat(fd, &st) < 0) { + close(fd); + return false; + } + + uintmax_t size = static_cast(st.st_size); + + if (size == 0) { + // Empty file, just close and delete it + close(fd); + boost::filesystem::remove(filepath); + return true; + } + + // Allocate buffer for overwriting (use 64KB chunks for efficiency) + const size_t BUFFER_SIZE = 65536; + std::vector buffer(BUFFER_SIZE); + + // Three-pass overwrite pattern (DoD 5220.22-M inspired) + const unsigned char patterns[3] = { + 0xFF, // Pass 1: All 1s (11111111) + 0xAA, // Pass 2: Alternating (10101010) + 0x00 // Pass 3: All 0s (00000000) + }; + + // Total work = 3 passes * fileSize + uintmax_t totalWork = size * 3; + uintmax_t completedWork = 0; + + for (int pass = 0; pass < 3; pass++) { + // Fill buffer with current pattern + std::memset(&buffer[0], patterns[pass], BUFFER_SIZE); + + // Seek to beginning of file + if (lseek(fd, 0, SEEK_SET) < 0) { + close(fd); + return false; + } + + // Overwrite entire file + uintmax_t remaining = size; + while (remaining > 0) { + size_t toWrite = (remaining > BUFFER_SIZE) ? BUFFER_SIZE : static_cast(remaining); + ssize_t bytesWritten = write(fd, &buffer[0], toWrite); + if (bytesWritten < 0 || static_cast(bytesWritten) != toWrite) { + close(fd); + return false; + } + remaining -= bytesWritten; + completedWork += bytesWritten; + + // Report progress + if (progressCallback) { + int percent = static_cast((completedWork * 100) / totalWork); + progressCallback(percent); + } + } + + // Flush to disk using the SAME file descriptor we wrote to + if (fsync(fd) < 0) { + close(fd); + return false; } } + + // Release the lock and close the file descriptor BEFORE rename/delete + fl.l_type = F_UNLCK; + fcntl(fd, F_SETLK, &fl); + close(fd); + fd = -1; + +#endif // Rename to obscure original filename before deletion boost::filesystem::path obscuredPath = filepath.parent_path() / "00000000000000000000000000000000";