fix(security): delete the plaintext key export after decrypt-wallet import (W2-1)

Removing wallet encryption z_exportwallet'd every private key in cleartext to
<datadir>/obsidiandecryptexport<ts>, re-imported it, and never deleted it — leaving a
full plaintext dump of every key on disk permanently. The decrypt flow now scrubs
(best-effort in-place zero-overwrite) and removes that file as soon as the
z_importwallet attempt resolves, on both the success and failure paths. Recovery, if
ever needed, remains the encrypted backup (wallet.dat.encrypted.bak), never this file.

Second fix in the wallet-hardening P0-A cluster (docs/wallet-hardening.md). Not
unit-testable (fs I/O in a deep worker lambda); build-clean, ctest 1/1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 14:02:45 -05:00
parent da0e9f5915
commit 9204fa148a
2 changed files with 24 additions and 0 deletions

View File

@@ -112,4 +112,5 @@ Land W7-2 first — it unblocks the rest.
## Progress log ## Progress log
- **P0-A / W2-1** — ☑ landed: the decrypt-wallet flow now scrubs (best-effort in-place zero-overwrite) and removes the plaintext key export (`obsidiandecryptexport…`) as soon as the `z_importwallet` attempt resolves — success or failure — so a full cleartext dump of every private key is no longer left on disk forever. Recovery remains the encrypted backup (`wallet.dat.encrypted.bak`). `app_security.cpp` (after the import call). Not unit-testable (fs I/O in a deep lambda); build-clean, `ctest` 1/1 (no regression).
- **P0-A / W7-1** — ☑ landed: `RedactConsoleCommand`/`ConsoleCommandCarriesSecret` in `console_tab_helpers` redact secret-bearing commands (an allowlist of 13 first-tokens: `walletpassphrase`, `encryptwallet`, `z_importkey`, …) to `> walletpassphrase ****` before they hit the console echo AND the recall history; the real command still executes unredacted. Wired into `submitConsoleCommand` (`console_tab.cpp`). New `testConsoleSecretRedaction` (11 assertions). Clean build; `ctest` 1/1. (Output-secret commands like `z_exportkey` — result redaction — remain a follow-up.) - **P0-A / W7-1** — ☑ landed: `RedactConsoleCommand`/`ConsoleCommandCarriesSecret` in `console_tab_helpers` redact secret-bearing commands (an allowlist of 13 first-tokens: `walletpassphrase`, `encryptwallet`, `z_importkey`, …) to `> walletpassphrase ****` before they hit the console echo AND the recall history; the real command still executes unredacted. Wired into `submitConsoleCommand` (`console_tab.cpp`). New `testConsoleSecretRedaction` (11 assertions). Clean build; `ctest` 1/1. (Output-secret commands like `z_exportkey` — result redaction — remain a follow-up.)

View File

@@ -33,6 +33,8 @@
#include <ctime> #include <ctime>
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <fstream>
#include <vector>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <utility> #include <utility>
@@ -1606,6 +1608,27 @@ void App::renderDecryptWalletDialog() {
WalletSecurityImportRpcAdapter importAdapter(rpc_.get(), saved_config_); WalletSecurityImportRpcAdapter importAdapter(rpc_.get(), saved_config_);
auto importResult = services::WalletSecurityWorkflowExecutor::importWallet( auto importResult = services::WalletSecurityWorkflowExecutor::importWallet(
importAdapter, exportPath); importAdapter, exportPath);
// The plaintext key export (obsidiandecryptexport…) has served its purpose now
// that the import attempt has resolved — scrub and remove it so a full cleartext
// dump of every private key isn't left on disk forever. Recovery, if ever needed,
// is the encrypted backup (wallet.dat.encrypted.bak), never this file.
{
std::error_code delEc;
const auto sz = std::filesystem::file_size(exportPath, delEc);
if (!delEc && sz > 0) {
std::fstream scrub(exportPath,
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(exportPath, delEc);
DEBUG_LOGF("[decrypt] removed plaintext key export after import\n");
}
if (!importResult.ok) { if (!importResult.ok) {
std::string err = importResult.error; std::string err = importResult.error;
if (worker_) { if (worker_) {