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"); diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index f6d31c1641f..e5dae838a79 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -20,9 +20,22 @@ #include #include +#include + +#include +#include +#include #include +#ifdef WIN32 +#include +#else +#include +#include +#include +#endif + #include "zcash/Address.hpp" using namespace std; @@ -483,6 +496,390 @@ 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) +{ + try { + // 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 = 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 + LARGE_INTEGER zero; + zero.QuadPart = 0; + if (!SetFilePointerEx(hFile, zero, NULL, FILE_BEGIN)) { + CloseHandle(hFile); + return false; + } + + // Overwrite entire file + uintmax_t remaining = size; + while (remaining > 0) { + 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 -= bytesWritten; + completedWork += bytesWritten; + + // Report progress + if (progressCallback) { + int percent = static_cast((completedWork * 100) / totalWork); + progressCallback(percent); + } + } + + // 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 + // 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"; + + // 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 +888,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 }, 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;