fix(security): scrub in-memory key/passphrase copies in the wallet secret paths (W4-1, W4-3, W2-3)
The wallet-hardening memzero cluster. Uses the file's established sodium_memzero pattern (matching the existing lambda-capture scrub at app_network.cpp:2885 and the JSON scrub at :4025) rather than a new type, since importPrivateKey/sweepPrivateKey are fund-moving code. - W4-1 importPrivateKey / sweepPrivateKey: the spending/viewing key was copied ≥3× (calling frame -> worker-lambda capture -> JSON params) and never scrubbed. Now zeroed on all paths: the calling-frame copy after the worker post, the lambda's captured copy (lambda made mutable, zeroed once the request is sent), and the request params copy. - W4-3 exportAllKeys / backupWallet: the concatenated all-keys buffer is now zeroed after the consumer uses it, and the backup is written via Platform::writeFileAtomically(..., restrictPermissions=true) — atomic and owner-only (0600) — instead of a umask-default std::ofstream that left it world-readable. - W2-3 decrypt-wallet passphrase: std::move-captured into the worker lambda (no plaintext copy left in the calling frame) and sodium_memzero'd right after unlockWallet, its only use. Not unit-testable (no observable RPC effect — the key value sent to the daemon is unchanged; only post-use memory zeroing is added). Build-clean; ctest 1/1. See docs/wallet-hardening.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ Status legend: ☐ not started · ◐ in progress · ☑ landed & verified
|
||||
|
||||
| Phase | Findings | Theme | Status |
|
||||
|-------|----------|-------|--------|
|
||||
| **P0-A** | W7-1, W2-1, W4-1, W4-3, W2-3, W4-5, W5-3 | Secret hardening (SecureString + console redaction + delete-export) | ◐ |
|
||||
| **P0-A** | W7-1, W2-1, W4-1, W4-3, W2-3 ✓ · W4-5, W5-3 ☐ | Secret hardening (console redaction + delete-export + memzero) | ◐ 5/7 |
|
||||
| **P0-B** | W2-2/W4-2, W2-4 | Encryption integrity (never silently unencrypted) | ☐ |
|
||||
| **P1-A** | W3-1, W3-2, W3-4, W3-3 | Migrate-to-seed correctness (fund-adjacent) | ☐ |
|
||||
| **P1-B** | W1-1, W1-3, W1-2, W1-4 | Missing/wrong wallet-file safety | ☐ |
|
||||
@@ -112,5 +112,10 @@ Land W7-2 first — it unblocks the rest.
|
||||
|
||||
## Progress log
|
||||
|
||||
- **P0-A / W4-1 · W4-3 · W2-3 (memzero cluster)** — ☑ landed, using the file's established `sodium_memzero` pattern (matching the existing lambda-capture scrub at app_network.cpp:2885 and JSON scrub at :4025) rather than a new type, since this is fund-moving code:
|
||||
- **W4-1** `importPrivateKey`/`sweepPrivateKey`: the spending/viewing key is now scrubbed on all paths — the calling-frame copy (after the worker post), the worker-lambda's captured copy (lambda made `mutable`, zeroed after the request is sent), and the JSON request `params` copy.
|
||||
- **W4-3** `exportAllKeys`/`backupWallet`: the concatenated all-keys buffer is zeroed after the consumer uses it, and the backup file is now written via `Platform::writeFileAtomically(..., restrictPermissions=true)` (atomic + 0600) instead of a umask-default `ofstream`.
|
||||
- **W2-3** decrypt-wallet passphrase: `std::move`-captured into the worker lambda (so no plaintext copy is left in the calling frame) and `sodium_memzero`'d right after `unlockWallet` (its only use).
|
||||
Not unit-testable (the scrubbing has no observable RPC effect — the key value sent to the daemon is unchanged; only post-use memory zeroing is added). Build-clean; `ctest` 1/1 (no regression). **Remaining in P0-A:** W5-3 (remove the dead lite `passphrase` field), W4-5 (predictable plaintext seed-backup file).
|
||||
- **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.)
|
||||
|
||||
Reference in New Issue
Block a user