diff --git a/docs/wallet-hardening.md b/docs/wallet-hardening.md index a827990..4ae649b 100644 --- a/docs/wallet-hardening.md +++ b/docs/wallet-hardening.md @@ -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 ". It now reads "**Saved an UNENCRYPTED seed file — move it to secure offline storage and delete this copy**: ", 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`. diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 3edee0f..52193ee 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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.";