fix(security): warn that the seed-backup file is unencrypted plaintext (W4-5)

The seed-phrase "Save" already wrote the file 0600 and zeroed the in-memory buffer, but
the success message was a bare "Saved to <path>" — no hint that it's a permanent
UNENCRYPTED copy of the seed at a predictable location. The message now reads
"Saved an UNENCRYPTED seed file — move it to secure offline storage and delete this
copy: <path>". English source updated; the res/lang back-fill of this changed key is
deferred to the batch i18n pass.

Also documents W5-3 (lite create-time passphrase) as a product decision rather than a
speculative change: the field is already wiped on every path (minimal security risk),
but the labeled masked "passphrase" input at lite create/open/restore is never consumed
by the backend — so either remove the dead UI or wire it into the lite encrypt flow.

Finishes the actionable part of the wallet-hardening P0-A cluster (docs/wallet-hardening.md).
Build-clean; ctest 1/1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 14:25:15 -05:00
parent f9b622cb25
commit 7e4822c021
2 changed files with 4 additions and 2 deletions

View File

@@ -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 (console redaction + delete-export + memzero) | ◐ 5/7 |
| **P0-A** | W7-1, W2-1, W4-1, W4-3, W2-3, W4-5 ✓ · W5-3 | Secret hardening (console redaction + delete-export + memzero) | ◐ 6/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,6 +112,8 @@ Land W7-2 first — it unblocks the rest.
## Progress log
- **P0-A / W5-3 (lite create-time passphrase)** — ⚑ **needs a product decision, not a speculative code change.** Investigation revised the finding: the passphrase field is already `secureWipeLiteSecret`'d on every path (controller:1175/1183/1192, settings_page:295), so the security exposure is minimal. The real issue is a **misleading UI**: `settings_page.cpp:1733-1739` renders a labeled, masked "passphrase" `InputText` at lite create/open/restore, but the backend `initialize*` calls (lifecycle_service:326/334/342) never consume it (lite encryption is a *separate* post-open `encrypt` flow). A user may believe their lite wallet is passphrase-protected at creation when it isn't. Two options, both product calls: **(a) remove** the dead field + its UI (settings_page 121/272/276/281/295/1733-1740, the request `passphrase` fields, the redaction/wipe refs), or **(b) wire** it into the lite `encrypt` flow so it actually protects the new wallet. Not changed unilaterally.
- **P0-A / W4-5 (seed-backup file)** — ☑ landed (proportionate): the seed "Save" already wrote 0600 + zeroed the in-memory buffer, but the success message was a bare "Saved to <path>". It now reads "**Saved an UNENCRYPTED seed file — move it to secure offline storage and delete this copy**: <path>", so the plaintext-on-disk risk is called out. `i18n.cpp` (English source; `res/lang` back-fill of this changed key is deferred to the batch i18n pass). A stronger fix (pre-save confirmation, or dropping the file-save in favor of on-screen + Copy) is a follow-up UX decision.
- **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`.

View File

@@ -320,7 +320,7 @@ void I18n::loadBuiltinEnglish()
strings_["seed_backup_load_failed"] = "Could not load the seed phrase.";
strings_["seed_backup_copy"] = "Copy";
strings_["seed_backup_save"] = "Save to file…";
strings_["seed_backup_saved"] = "Saved to ";
strings_["seed_backup_saved"] = "Saved an UNENCRYPTED seed file — move it to secure offline storage and delete this copy: ";
strings_["seed_backup_save_failed"] = "Could not write ";
strings_["seed_backup_close"] = "Close";
strings_["seed_backup_reminder"] = "Your wallet has a 24-word recovery seed phrase. Back it up now in Settings → Node & Security.";